diff --git a/.github/workflows/diffcalc.yml b/.github/workflows/diffcalc.yml index 3b77a463c1..c2eeff20df 100644 --- a/.github/workflows/diffcalc.yml +++ b/.github/workflows/diffcalc.yml @@ -341,9 +341,12 @@ jobs: sed -i 's/^GH_TOKEN=.*$/GH_TOKEN=${{ github.token }}/' "${{ needs.directory.outputs.GENERATOR_ENV }}" cd "${{ needs.directory.outputs.GENERATOR_DIR }}" - docker-compose up --build generator - link=$(docker-compose logs generator -n 10 | grep 'http' | sed -E 's/^.*(http.*)$/\1/') + docker compose up --build --detach + docker compose logs --follow & + docker compose wait generator + + link=$(docker compose logs --tail 10 generator | grep 'http' | sed -E 's/^.*(http.*)$/\1/') target=$(cat "${{ needs.directory.outputs.GENERATOR_ENV }}" | grep -E '^OSU_B=' | cut -d '=' -f2-) echo "TARGET=${target}" >> "${GITHUB_OUTPUT}" @@ -353,7 +356,7 @@ jobs: if: ${{ always() }} run: | cd "${{ needs.directory.outputs.GENERATOR_DIR }}" - docker-compose down -v + docker compose down --volumes output-cli: name: Output info diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj index 7d43eb2b05..f77cda1533 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj @@ -9,7 +9,7 @@ false - + diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index 7dc8a1336b..47cabaddb1 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -9,7 +9,7 @@ false - + diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj index 9c4c8217f0..a7d62291d0 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj @@ -9,7 +9,7 @@ false - + diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index 7dc8a1336b..47cabaddb1 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -9,7 +9,7 @@ false - + diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj index af84ee47f1..9764c71493 100644 --- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj +++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj @@ -7,7 +7,7 @@ - + diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj index 619081c754..b434d6aaf9 100644 --- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj +++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj @@ -1,7 +1,7 @@  - + diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj index eee06acdb8..e7abd47881 100644 --- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj +++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj @@ -1,7 +1,7 @@  - + diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs index b70ecfbba8..fb109ba6f9 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs @@ -9,6 +9,7 @@ using osu.Framework.Utils; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Tests.Visual; using osu.Game.Utils; @@ -129,6 +130,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private void gridActive(bool active) where T : PositionSnapGrid { + AddAssert($"grid type is {typeof(T).Name}", () => this.ChildrenOfType().Any()); AddStep("choose placement tool", () => InputManager.Key(Key.Number2)); AddStep("move cursor to spacing + (1, 1)", () => { @@ -161,7 +163,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), + 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 }; @@ -170,7 +173,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor [Test] public void TestGridSizeToggling() { - AddStep("enable rectangular grid", () => InputManager.Key(Key.T)); + AddStep("enable rectangular grid", () => InputManager.Key(Key.Y)); AddUntilStep("rectangular grid visible", () => this.ChildrenOfType().Any()); gridSizeIs(4); @@ -189,5 +192,97 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private void gridSizeIs(int size) => AddAssert($"grid size is {size}", () => this.ChildrenOfType().Single().Spacing.Value == new Vector2(size) && EditorBeatmap.BeatmapInfo.GridSize == size); + + [Test] + public void TestGridTypeToggling() + { + AddStep("enable rectangular grid", () => InputManager.Key(Key.T)); + AddUntilStep("rectangular grid visible", () => this.ChildrenOfType().Any()); + gridActive(true); + + nextGridTypeIs(); + nextGridTypeIs(); + nextGridTypeIs(); + } + + private void nextGridTypeIs() where T : PositionSnapGrid + { + AddStep("toggle to next grid type", () => + { + InputManager.PressKey(Key.ShiftLeft); + InputManager.Key(Key.G); + InputManager.ReleaseKey(Key.ShiftLeft); + }); + gridActive(true); + } + + [Test] + public void TestGridPlacementTool() + { + AddStep("enable rectangular grid", () => InputManager.Key(Key.T)); + + AddStep("start grid placement", () => InputManager.Key(Key.Number5)); + AddStep("move cursor to slider head + (1, 1)", () => + { + var composer = Editor.ChildrenOfType().Single(); + InputManager.MoveMouseTo(composer.ToScreenSpace(((Slider)EditorBeatmap.HitObjects.First()).Position + new Vector2(1, 1))); + }); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddStep("move cursor to slider tail + (1, 1)", () => + { + var composer = Editor.ChildrenOfType().Single(); + InputManager.MoveMouseTo(composer.ToScreenSpace(((Slider)EditorBeatmap.HitObjects.First()).EndPosition + new Vector2(1, 1))); + }); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + + gridActive(true); + AddAssert("grid position at slider head", () => + { + var composer = Editor.ChildrenOfType().Single(); + return Precision.AlmostEquals(((Slider)EditorBeatmap.HitObjects.First()).Position, composer.StartPosition.Value); + }); + AddAssert("grid spacing is distance to slider tail", () => + { + var composer = Editor.ChildrenOfType().Single(); + return Precision.AlmostEquals(composer.Spacing.Value.X, 32.05, 0.01) + && Precision.AlmostEquals(composer.Spacing.Value.X, composer.Spacing.Value.Y); + }); + AddAssert("grid rotation points to slider tail", () => + { + var composer = Editor.ChildrenOfType().Single(); + return Precision.AlmostEquals(composer.GridLineRotation.Value, 0.09, 0.01); + }); + + AddStep("start grid placement", () => InputManager.Key(Key.Number5)); + AddStep("move cursor to slider tail + (1, 1)", () => + { + var composer = Editor.ChildrenOfType().Single(); + InputManager.MoveMouseTo(composer.ToScreenSpace(((Slider)EditorBeatmap.HitObjects.First()).EndPosition + new Vector2(1, 1))); + }); + AddStep("double click", () => + { + InputManager.Click(MouseButton.Left); + InputManager.Click(MouseButton.Left); + }); + AddStep("move cursor to (0, 0)", () => + { + var composer = Editor.ChildrenOfType().Single(); + InputManager.MoveMouseTo(composer.ToScreenSpace(Vector2.Zero)); + }); + + gridActive(true); + AddAssert("grid position at slider tail", () => + { + var composer = Editor.ChildrenOfType().Single(); + return Precision.AlmostEquals(((Slider)EditorBeatmap.HitObjects.First()).EndPosition, composer.StartPosition.Value); + }); + AddAssert("grid spacing and rotation unchanged", () => + { + var composer = Editor.ChildrenOfType().Single(); + return Precision.AlmostEquals(composer.Spacing.Value.X, 32.05, 0.01) + && Precision.AlmostEquals(composer.Spacing.Value.X, composer.Spacing.Value.Y) + && Precision.AlmostEquals(composer.GridLineRotation.Value, 0.09, 0.01); + }); + } } } diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModMirror.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModMirror.cs new file mode 100644 index 0000000000..0b3496ba68 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModMirror.cs @@ -0,0 +1,64 @@ +// 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.Framework.Utils; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.UI; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests.Mods +{ + public partial class TestSceneOsuModMirror : OsuModTestScene + { + [Test] + public void TestCorrectReflections([Values] OsuModMirror.MirrorType type, [Values] bool withStrictTracking) => CreateModTest(new ModTestData + { + Autoplay = true, + Beatmap = new OsuBeatmap + { + HitObjects = + { + new Slider + { + Position = new Vector2(0), + Path = new SliderPath + { + ControlPoints = + { + new PathControlPoint(), + new PathControlPoint(new Vector2(100, 0)) + } + }, + TickDistanceMultiplier = 0.5, + RepeatCount = 1, + } + } + }, + Mods = withStrictTracking + ? [new OsuModMirror { Reflection = { Value = type } }, new OsuModStrictTracking()] + : [new OsuModMirror { Reflection = { Value = type } }], + PassCondition = () => + { + var slider = this.ChildrenOfType().SingleOrDefault(); + var playfield = this.ChildrenOfType().Single(); + + if (slider == null) + return false; + + return Precision.AlmostEquals(playfield.ToLocalSpace(slider.HeadCircle.ScreenSpaceDrawQuad.Centre), slider.HitObject.Position) + && Precision.AlmostEquals(playfield.ToLocalSpace(slider.TailCircle.ScreenSpaceDrawQuad.Centre), slider.HitObject.Position) + && Precision.AlmostEquals(playfield.ToLocalSpace(slider.NestedHitObjects.OfType().Single().ScreenSpaceDrawQuad.Centre), + slider.HitObject.Position + slider.HitObject.Path.PositionAt(1)) + && Precision.AlmostEquals(playfield.ToLocalSpace(slider.NestedHitObjects.OfType().First().ScreenSpaceDrawQuad.Centre), + slider.HitObject.Position + slider.HitObject.Path.PositionAt(0.7f)); + } + }); + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj index ea54c8d313..5ea231e606 100644 --- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj +++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj @@ -1,7 +1,7 @@  - + diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index 2b88860cc8..768a764ad1 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -213,6 +213,8 @@ namespace osu.Game.Rulesets.Osu.Edit { GridLinesRotation.Disabled = v.NewValue == PositionSnapGridType.Circle; + gridTypeButtons.Items[(int)v.NewValue].Select(); + switch (v.NewValue) { case PositionSnapGridType.Square: @@ -241,17 +243,16 @@ namespace osu.Game.Rulesets.Osu.Edit return ((rotation + 360 + period * 0.5f) % period) - period * 0.5f; } - private void nextGridSize() - { - Spacing.Value = Spacing.Value * 2 >= max_automatic_spacing ? Spacing.Value / 8 : Spacing.Value * 2; - } - public bool OnPressed(KeyBindingPressEvent e) { switch (e.Action) { - case GlobalAction.EditorCycleGridDisplayMode: - nextGridSize(); + case GlobalAction.EditorCycleGridSpacing: + Spacing.Value = Spacing.Value * 2 >= max_automatic_spacing ? Spacing.Value / 8 : Spacing.Value * 2; + return true; + + case GlobalAction.EditorCycleGridType: + GridType.Value = (PositionSnapGridType)(((int)GridType.Value + 1) % Enum.GetValues().Length); return true; } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index fc85865dd2..e3ab95c402 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -240,39 +240,74 @@ namespace osu.Game.Rulesets.Osu.Edit points = originalConvexHull!; foreach (var point in points) - { - scale = clampToBound(scale, point, Vector2.Zero); - scale = clampToBound(scale, point, OsuPlayfield.BASE_SIZE); - } + scale = clampToBounds(scale, point, Vector2.Zero, OsuPlayfield.BASE_SIZE); - return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON)); + return scale; - float minPositiveComponent(Vector2 v) => MathF.Min(v.X < 0 ? float.PositiveInfinity : v.X, v.Y < 0 ? float.PositiveInfinity : v.Y); - - Vector2 clampToBound(Vector2 s, Vector2 p, Vector2 bound) + // Clamps the scale vector s such that the point p scaled by s is within the rectangle defined by lowerBounds and upperBounds + Vector2 clampToBounds(Vector2 s, Vector2 p, Vector2 lowerBounds, Vector2 upperBounds) { p -= actualOrigin; - bound -= actualOrigin; + lowerBounds -= actualOrigin; + upperBounds -= actualOrigin; + // a.X is the rotated X component of p with respect to the X bounds + // a.Y is the rotated X component of p with respect to the Y bounds + // b.X is the rotated Y component of p with respect to the X bounds + // b.Y is the rotated Y component of p with respect to the Y bounds var a = new Vector2(cos * cos * p.X - sin * cos * p.Y, -sin * cos * p.X + sin * sin * p.Y); var b = new Vector2(sin * sin * p.X + sin * cos * p.Y, sin * cos * p.X + cos * cos * p.Y); + float sLowerBound, sUpperBound; + switch (adjustAxis) { case Axes.X: - s.X = MathF.Min(scale.X, minPositiveComponent(Vector2.Divide(bound - b, a))); + (sLowerBound, sUpperBound) = computeBounds(lowerBounds - b, upperBounds - b, a); + s.X = MathHelper.Clamp(s.X, sLowerBound, sUpperBound); break; case Axes.Y: - s.Y = MathF.Min(scale.Y, minPositiveComponent(Vector2.Divide(bound - a, b))); + (sLowerBound, sUpperBound) = computeBounds(lowerBounds - a, upperBounds - a, b); + s.Y = MathHelper.Clamp(s.Y, sLowerBound, sUpperBound); break; case Axes.Both: - s = Vector2.ComponentMin(s, s * minPositiveComponent(Vector2.Divide(bound, a * s.X + b * s.Y))); + // Here we compute the bounds for the magnitude multiplier of the scale vector + // Therefore the ratio s.X / s.Y will be maintained + (sLowerBound, sUpperBound) = computeBounds(lowerBounds, upperBounds, a * s.X + b * s.Y); + s.X = s.X < 0 + ? MathHelper.Clamp(s.X, s.X * sUpperBound, s.X * sLowerBound) + : MathHelper.Clamp(s.X, s.X * sLowerBound, s.X * sUpperBound); + s.Y = s.Y < 0 + ? MathHelper.Clamp(s.Y, s.Y * sUpperBound, s.Y * sLowerBound) + : MathHelper.Clamp(s.Y, s.Y * sLowerBound, s.Y * sUpperBound); break; } return s; } + + // Computes the bounds for the magnitude of the scaled point p with respect to the bounds lowerBounds and upperBounds + (float, float) computeBounds(Vector2 lowerBounds, Vector2 upperBounds, Vector2 p) + { + var sLowerBounds = Vector2.Divide(lowerBounds, p); + var sUpperBounds = Vector2.Divide(upperBounds, p); + + // If the point is negative, then the bounds are flipped + if (p.X < 0) + (sLowerBounds.X, sUpperBounds.X) = (sUpperBounds.X, sLowerBounds.X); + if (p.Y < 0) + (sLowerBounds.Y, sUpperBounds.Y) = (sUpperBounds.Y, sLowerBounds.Y); + + // If the point is at zero, then any scale will have no effect on the point so the bounds are infinite + // The float division would already give us infinity for the bounds, but the sign is not consistent so we have to manually set it + if (Precision.AlmostEquals(p.X, 0)) + (sLowerBounds.X, sUpperBounds.X) = (float.NegativeInfinity, float.PositiveInfinity); + if (Precision.AlmostEquals(p.Y, 0)) + (sLowerBounds.Y, sUpperBounds.Y) = (float.NegativeInfinity, float.PositiveInfinity); + + return (MathF.Max(sLowerBounds.X, sLowerBounds.Y), MathF.Min(sUpperBounds.X, sUpperBounds.Y)); + } } private void moveSelectionInBounds() diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index 33b0c14185..b2c48ae326 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -136,8 +136,26 @@ namespace osu.Game.Rulesets.Osu.Edit }); scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue }); - xCheckBox.Current.BindValueChanged(x => setAxis(x.NewValue, yCheckBox.Current.Value)); - yCheckBox.Current.BindValueChanged(y => setAxis(xCheckBox.Current.Value, y.NewValue)); + xCheckBox.Current.BindValueChanged(_ => + { + if (!xCheckBox.Current.Value && !yCheckBox.Current.Value) + { + yCheckBox.Current.Value = true; + return; + } + + updateAxes(); + }); + yCheckBox.Current.BindValueChanged(_ => + { + if (!xCheckBox.Current.Value && !yCheckBox.Current.Value) + { + xCheckBox.Current.Value = true; + return; + } + + updateAxes(); + }); selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleX.Value || scaleHandler.CanScaleY.Value); playfieldCentreButton.Selected.Disabled = scaleHandler.IsScalingSlider.Value && !selectionCentreButton.Selected.Disabled; @@ -152,6 +170,12 @@ namespace osu.Game.Rulesets.Osu.Edit }); } + private void updateAxes() + { + scaleInfo.Value = scaleInfo.Value with { XAxis = xCheckBox.Current.Value, YAxis = yCheckBox.Current.Value }; + updateMinMaxScale(); + } + private void updateAxisCheckBoxesEnabled() { if (scaleInfo.Value.Origin != ScaleOrigin.SelectionCentre) @@ -175,12 +199,14 @@ namespace osu.Game.Rulesets.Osu.Edit axisBindable.Disabled = !available; } - private void updateMaxScale() + private void updateMinMaxScale() { if (!scaleHandler.OriginalSurroundingQuad.HasValue) return; + const float min_scale = 0.5f; const float max_scale = 10; + var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value), getAdjustAxis(scaleInfo.Value), getRotation(scaleInfo.Value)); if (!scaleInfo.Value.XAxis) @@ -189,12 +215,21 @@ namespace osu.Game.Rulesets.Osu.Edit scale.Y = max_scale; scaleInputBindable.MaxValue = MathF.Max(1, MathF.Min(scale.X, scale.Y)); + + scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(min_scale), getOriginPosition(scaleInfo.Value), getAdjustAxis(scaleInfo.Value), getRotation(scaleInfo.Value)); + + if (!scaleInfo.Value.XAxis) + scale.X = min_scale; + if (!scaleInfo.Value.YAxis) + scale.Y = min_scale; + + scaleInputBindable.MinValue = MathF.Min(1, MathF.Max(scale.X, scale.Y)); } private void setOrigin(ScaleOrigin origin) { scaleInfo.Value = scaleInfo.Value with { Origin = origin }; - updateMaxScale(); + updateMinMaxScale(); updateAxisCheckBoxesEnabled(); } @@ -219,21 +254,26 @@ namespace osu.Game.Rulesets.Osu.Edit } } - private Axes getAdjustAxis(PreciseScaleInfo scale) => scale.XAxis ? scale.YAxis ? Axes.Both : Axes.X : Axes.Y; + private Axes getAdjustAxis(PreciseScaleInfo scale) + { + var result = Axes.None; + + if (scale.XAxis) + result |= Axes.X; + + if (scale.YAxis) + result |= Axes.Y; + + return result; + } private float getRotation(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.GridCentre ? gridToolbox.GridLinesRotation.Value : 0; - private void setAxis(bool x, bool y) - { - scaleInfo.Value = scaleInfo.Value with { XAxis = x, YAxis = y }; - updateMaxScale(); - } - protected override void PopIn() { base.PopIn(); scaleHandler.Begin(); - updateMaxScale(); + updateMinMaxScale(); } protected override void PopOut() diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs index 2394cf92fc..8898faf7b8 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs @@ -2,10 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu.Mods { @@ -25,5 +28,14 @@ namespace osu.Game.Rulesets.Osu.Mods } }; } + + public override void Update(Playfield playfield) + { + base.Update(playfield); + OsuPlayfield osuPlayfield = (OsuPlayfield)playfield; + Debug.Assert(osuPlayfield.Cursor != null); + + osuPlayfield.Cursor.ActiveCursor.Rotation = -CurrentRotation; + } } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs index 2c9292c58b..7d2fd628f6 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs @@ -120,6 +120,7 @@ namespace osu.Game.Rulesets.Osu.Mods Position = Position + Path.PositionAt(e.PathProgress), StackHeight = StackHeight, Scale = Scale, + PathProgress = e.PathProgress, }); break; @@ -150,6 +151,7 @@ namespace osu.Game.Rulesets.Osu.Mods Position = Position + Path.PositionAt(e.PathProgress), StackHeight = StackHeight, Scale = Scale, + PathProgress = e.PathProgress, }); break; } diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs index e936c24c08..f27624a633 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs @@ -3,7 +3,6 @@ using System; using System.Linq; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Beatmaps; @@ -117,10 +116,9 @@ namespace osu.Game.Rulesets.Osu.Utils if (osuObject is not Slider slider) return; - void reflectNestedObject(OsuHitObject nested) => nested.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - nested.Position.X, nested.Position.Y); static void reflectControlPoint(PathControlPoint point) => point.Position = new Vector2(-point.Position.X, point.Position.Y); - modifySlider(slider, reflectNestedObject, reflectControlPoint); + modifySlider(slider, reflectControlPoint); } /// @@ -134,10 +132,9 @@ namespace osu.Game.Rulesets.Osu.Utils if (osuObject is not Slider slider) return; - void reflectNestedObject(OsuHitObject nested) => nested.Position = new Vector2(nested.Position.X, OsuPlayfield.BASE_SIZE.Y - nested.Position.Y); static void reflectControlPoint(PathControlPoint point) => point.Position = new Vector2(point.Position.X, -point.Position.Y); - modifySlider(slider, reflectNestedObject, reflectControlPoint); + modifySlider(slider, reflectControlPoint); } /// @@ -146,10 +143,9 @@ namespace osu.Game.Rulesets.Osu.Utils /// The slider to be flipped. public static void FlipSliderInPlaceHorizontally(Slider slider) { - void flipNestedObject(OsuHitObject nested) => nested.Position = new Vector2(slider.X - (nested.X - slider.X), nested.Y); static void flipControlPoint(PathControlPoint point) => point.Position = new Vector2(-point.Position.X, point.Position.Y); - modifySlider(slider, flipNestedObject, flipControlPoint); + modifySlider(slider, flipControlPoint); } /// @@ -159,18 +155,13 @@ namespace osu.Game.Rulesets.Osu.Utils /// The angle, measured in radians, to rotate the slider by. public static void RotateSlider(Slider slider, float rotation) { - void rotateNestedObject(OsuHitObject nested) => nested.Position = rotateVector(nested.Position - slider.Position, rotation) + slider.Position; void rotateControlPoint(PathControlPoint point) => point.Position = rotateVector(point.Position, rotation); - modifySlider(slider, rotateNestedObject, rotateControlPoint); + modifySlider(slider, rotateControlPoint); } - private static void modifySlider(Slider slider, Action modifyNestedObject, Action modifyControlPoint) + private static void modifySlider(Slider slider, Action modifyControlPoint) { - // No need to update the head and tail circles, since slider handles that when the new slider path is set - slider.NestedHitObjects.OfType().ForEach(modifyNestedObject); - slider.NestedHitObjects.OfType().ForEach(modifyNestedObject); - var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray(); foreach (var point in controlPoints) modifyControlPoint(point); diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj index a2420fc679..2170009ae8 100644 --- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj +++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj @@ -1,7 +1,7 @@  - + diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index 0eac70f9c8..38746f2567 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -716,7 +716,7 @@ namespace osu.Game.Tests.Database { foreach (var entry in zip.Entries.ToArray()) { - if (entry.Key.EndsWith(".osu", StringComparison.InvariantCulture)) + if (entry.Key!.EndsWith(".osu", StringComparison.InvariantCulture)) zip.RemoveEntry(entry); } diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index 9ecfa72947..c8f063719d 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -627,6 +627,87 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.AreEqual(DateTimeOffset.MinValue.AddMilliseconds(1), filterCriteria.LastPlayed.Min); } + private static DateTimeOffset dateTimeOffsetFromDateOnly(int year, int month, int day) => + new DateTimeOffset(year, month, day, 0, 0, 0, TimeSpan.Zero); + + private static readonly object[] ranked_date_valid_test_cases = + { + new object[] { "ranked<2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Max }, + new object[] { "ranked<2012.03", dateTimeOffsetFromDateOnly(2012, 3, 1), (FilterCriteria x) => x.DateRanked.Max }, + new object[] { "ranked<2012/03/05", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Max }, + new object[] { "ranked<2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Max }, + + new object[] { "ranked<=2012", dateTimeOffsetFromDateOnly(2013, 1, 1), (FilterCriteria x) => x.DateRanked.Max }, + new object[] { "ranked<=2012.03", dateTimeOffsetFromDateOnly(2012, 4, 1), (FilterCriteria x) => x.DateRanked.Max }, + new object[] { "ranked<=2012/03/05", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Max }, + new object[] { "ranked<=2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Max }, + + new object[] { "ranked>2012", dateTimeOffsetFromDateOnly(2013, 1, 1), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked>2012.03", dateTimeOffsetFromDateOnly(2012, 4, 1), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked>2012/03/05", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked>2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Min }, + + new object[] { "ranked>=2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked>=2012.03", dateTimeOffsetFromDateOnly(2012, 3, 1), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked>=2012/03/05", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked>=2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Min }, + + new object[] { "ranked=2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked=2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked=2012-03", dateTimeOffsetFromDateOnly(2012, 3, 1), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked=2012-03", dateTimeOffsetFromDateOnly(2012, 4, 1), (FilterCriteria x) => x.DateRanked.Max }, + new object[] { "ranked=2012-03-05", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked=2012-03-05", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Max }, + }; + + [Test] + [TestCaseSource(nameof(ranked_date_valid_test_cases))] + public void TestValidRankedDateQueries(string query, DateTimeOffset expected, Func f) + { + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual(true, filterCriteria.DateRanked.HasFilter); + Assert.AreEqual(expected, f(filterCriteria)); + } + + private static readonly object[] ranked_date_invalid_test_cases = + { + new object[] { "ranked<0" }, + new object[] { "ranked=99999" }, + new object[] { "ranked>=2012-03-05-04" }, + }; + + [Test] + [TestCaseSource(nameof(ranked_date_invalid_test_cases))] + public void TestInvalidRankedDateQueries(string query) + { + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual(false, filterCriteria.DateRanked.HasFilter); + } + + private static readonly object[] submitted_date_test_cases = + { + new object[] { "submitted<2012", true }, + new object[] { "submitted<2012.03", true }, + new object[] { "submitted<2012/03/05", true }, + new object[] { "submitted<2012-3-5", true }, + + new object[] { "submitted<0", false }, + new object[] { "submitted=99999", false }, + new object[] { "submitted>=2012-03-05-04", false }, + new object[] { "submitted>=2012/03.05-04", false }, + }; + + [Test] + [TestCaseSource(nameof(submitted_date_test_cases))] + public void TestInvalidRankedDateQueries(string query, bool expected) + { + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual(expected, filterCriteria.DateSubmitted.HasFilter); + } + private static readonly object[] played_query_tests = { new object[] { "0", DateTimeOffset.MinValue, true }, diff --git a/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs index 33f4d577bd..ad0c5f9247 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs @@ -8,11 +8,13 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; 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.Testing; using osu.Framework.Utils; +using osu.Game.Graphics.Sprites; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; @@ -90,6 +92,48 @@ namespace osu.Game.Tests.Visual.Online AddAssert("no best score displayed", () => scoresContainer.ChildrenOfType().Count() == 1); } + [Test] + public void TestHitResultsWithSameNameAreGrouped() + { + AddStep("Load scores without user best", () => + { + var allScores = createScores(); + allScores.UserScore = null; + scoresContainer.Scores = allScores; + }); + + AddUntilStep("wait for scores displayed", () => scoresContainer.ChildrenOfType().Any()); + AddAssert("only one column for slider end", () => + { + ScoreTable scoreTable = scoresContainer.ChildrenOfType().First(); + return scoreTable.Columns.Count(c => c.Header.Equals("slider end")) == 1; + }); + + AddAssert("all rows show non-zero slider ends", () => + { + ScoreTable scoreTable = scoresContainer.ChildrenOfType().First(); + int sliderEndColumnIndex = Array.FindIndex(scoreTable.Columns, c => c != null && c.Header.Equals("slider end")); + bool sliderEndFilledInEachRow = true; + + for (int i = 0; i < scoreTable.Content?.GetLength(0); i++) + { + switch (scoreTable.Content[i, sliderEndColumnIndex]) + { + case OsuSpriteText text: + if (text.Text.Equals(0.0d.ToLocalisableString(@"N0"))) + sliderEndFilledInEachRow = false; + break; + + default: + sliderEndFilledInEachRow = false; + break; + } + } + + return sliderEndFilledInEachRow; + }); + } + [Test] public void TestUserBest() { @@ -103,6 +147,18 @@ namespace osu.Game.Tests.Visual.Online AddUntilStep("wait for scores displayed", () => scoresContainer.ChildrenOfType().Any()); AddAssert("best score displayed", () => scoresContainer.ChildrenOfType().Count() == 2); + AddStep("Load scores with personal best FC", () => + { + var allScores = createScores(); + allScores.UserScore = createUserBest(); + allScores.UserScore.Score.Accuracy = 1; + scoresContainer.Beatmap.Value.MaxCombo = allScores.UserScore.Score.MaxCombo = 1337; + scoresContainer.Scores = allScores; + }); + + AddUntilStep("wait for scores displayed", () => scoresContainer.ChildrenOfType().Any()); + AddAssert("best score displayed", () => scoresContainer.ChildrenOfType().Count() == 2); + AddStep("Load scores with personal best (null position)", () => { var allScores = createScores(); @@ -287,13 +343,17 @@ namespace osu.Game.Tests.Visual.Online const int initial_great_count = 2000; const int initial_tick_count = 100; + const int initial_slider_end_count = 500; int greatCount = initial_great_count; int tickCount = initial_tick_count; + int sliderEndCount = initial_slider_end_count; - foreach (var s in scores.Scores) + foreach (var (score, index) in scores.Scores.Select((s, i) => (s, i))) { - s.Statistics = new Dictionary + HitResult sliderEndResult = index % 2 == 0 ? HitResult.SliderTailHit : HitResult.SmallTickHit; + + score.Statistics = new Dictionary { { HitResult.Great, greatCount }, { HitResult.LargeTickHit, tickCount }, @@ -301,10 +361,19 @@ namespace osu.Game.Tests.Visual.Online { HitResult.Meh, RNG.Next(100) }, { HitResult.Miss, initial_great_count - greatCount }, { HitResult.LargeTickMiss, initial_tick_count - tickCount }, + { sliderEndResult, sliderEndCount }, + }; + + // Some hit results, including SliderTailHit and SmallTickHit, are only displayed + // when the maximum number is known + score.MaximumStatistics = new Dictionary + { + { sliderEndResult, initial_slider_end_count }, }; greatCount -= 100; tickCount -= RNG.Next(1, 5); + sliderEndCount -= 20; } return scores; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs new file mode 100644 index 0000000000..bf959d9862 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapAttributeText.cs @@ -0,0 +1,98 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Models; +using osu.Game.Rulesets.Osu; +using osu.Game.Skinning.Components; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public partial class TestSceneBeatmapAttributeText : OsuTestScene + { + private readonly BeatmapAttributeText text; + + public TestSceneBeatmapAttributeText() + { + Child = text = new BeatmapAttributeText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }; + } + + [SetUp] + public void Setup() => Schedule(() => + { + Beatmap.Value = CreateWorkingBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo) + { + BeatmapInfo = + { + BPM = 100, + DifficultyName = "_Difficulty", + Status = BeatmapOnlineStatus.Loved, + Metadata = + { + Title = "_Title", + TitleUnicode = "_Title", + Artist = "_Artist", + ArtistUnicode = "_Artist", + Author = new RealmUser { Username = "_Creator" }, + Source = "_Source", + }, + Difficulty = + { + CircleSize = 1, + DrainRate = 2, + OverallDifficulty = 3, + ApproachRate = 4, + } + } + }); + }); + + [TestCase(BeatmapAttribute.CircleSize, "Circle Size: 1.00")] + [TestCase(BeatmapAttribute.HPDrain, "HP Drain: 2.00")] + [TestCase(BeatmapAttribute.Accuracy, "Accuracy: 3.00")] + [TestCase(BeatmapAttribute.ApproachRate, "Approach Rate: 4.00")] + [TestCase(BeatmapAttribute.Title, "Title: _Title")] + [TestCase(BeatmapAttribute.Artist, "Artist: _Artist")] + [TestCase(BeatmapAttribute.Creator, "Creator: _Creator")] + [TestCase(BeatmapAttribute.DifficultyName, "Difficulty: _Difficulty")] + [TestCase(BeatmapAttribute.Source, "Source: _Source")] + [TestCase(BeatmapAttribute.RankedStatus, "Beatmap Status: Loved")] + public void TestAttributeDisplay(BeatmapAttribute attribute, string expectedText) + { + AddStep($"set attribute: {attribute}", () => text.Attribute.Value = attribute); + AddAssert("check correct text", getText, () => Is.EqualTo(expectedText)); + } + + [Test] + public void TestChangeBeatmap() + { + AddStep("set title attribute", () => text.Attribute.Value = BeatmapAttribute.Title); + AddAssert("check initial title", getText, () => Is.EqualTo("Title: _Title")); + + AddStep("change to beatmap with another title", () => Beatmap.Value = CreateWorkingBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo) + { + BeatmapInfo = + { + Metadata = + { + Title = "Another" + } + } + })); + + AddAssert("check new title", getText, () => Is.EqualTo("Title: Another")); + } + + private string getText() => text.ChildrenOfType().Single().Text.ToString(); + } +} diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index c0bbdfb4ed..28a1d4d021 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -1,8 +1,8 @@  + - diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index 8f1d7114b1..04683cd83b 100644 --- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj +++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj @@ -4,7 +4,7 @@ osu.Game.Tournament.Tests.TournamentTestRunner - + diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 425fd98d27..f1463eb632 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -62,7 +62,7 @@ namespace osu.Game.Beatmaps } [UsedImplicitly] - private BeatmapInfo() + protected BeatmapInfo() { } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index cd818941ff..4191771116 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -285,7 +285,8 @@ namespace osu.Game.Beatmaps /// /// The query. /// The first result for the provided query, or null if no results were found. - public BeatmapInfo? QueryBeatmap(Expression> query) => Realm.Run(r => r.All().Filter($"{nameof(BeatmapInfo.BeatmapSet)}.{nameof(BeatmapSetInfo.DeletePending)} == false").FirstOrDefault(query)?.Detach()); + public BeatmapInfo? QueryBeatmap(Expression> query) => Realm.Run(r => + r.All().Filter($"{nameof(BeatmapInfo.BeatmapSet)}.{nameof(BeatmapSetInfo.DeletePending)} == false").FirstOrDefault(query)?.Detach()); /// /// A default representation of a WorkingBeatmap to use when no beatmap is available. @@ -313,6 +314,23 @@ namespace osu.Game.Beatmaps }); } + public void ResetAllOffsets() + { + const string reset_complete_message = "All offsets have been reset!"; + Realm.Write(r => + { + var items = r.All(); + + foreach (var beatmap in items) + { + if (beatmap.UserSettings.Offset != 0) + beatmap.UserSettings.Offset = 0; + } + + PostNotification?.Invoke(new ProgressCompletionNotification { Text = reset_complete_message }); + }); + } + public void Delete(Expression>? filter = null, bool silent = false) { Realm.Run(r => diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index ad0423191d..eb7182820b 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -93,8 +93,9 @@ namespace osu.Game.Database /// 40 2023-12-21 Add ScoreInfo.Version to keep track of which build scores were set on. /// 41 2024-04-17 Add ScoreInfo.TotalScoreWithoutMods for future mod multiplier rebalances. /// 42 2024-08-07 Update mania key bindings to reflect changes to ManiaAction + /// 43 2024-10-14 Reset keybind for toggling FPS display to avoid conflict with "convert to stream" in the editor, if not already changed by user. /// - private const int schema_version = 42; + private const int schema_version = 43; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. @@ -375,10 +376,6 @@ namespace osu.Game.Database { foreach (var beatmap in beatmapSet.Beatmaps) { - // Cascade delete related scores, else they will have a null beatmap against the model's spec. - foreach (var score in beatmap.Scores) - realm.Remove(score); - realm.Remove(beatmap.Metadata); realm.Remove(beatmap); } @@ -1192,6 +1189,21 @@ namespace osu.Game.Database } break; + + case 43: + { + // Clear default bindings for "Toggle FPS Display", + // as it conflicts with "Convert to Stream" in the editor. + // Only apply change if set to the conflicting bind + // i.e. has been manually rebound by the user. + var keyBindings = migration.NewRealm.All(); + + var toggleFpsBind = keyBindings.FirstOrDefault(bind => bind.ActionInt == (int)GlobalAction.ToggleFPSDisplay); + if (toggleFpsBind != null && toggleFpsBind.KeyCombination.Keys.SequenceEqual(new[] { InputKey.Shift, InputKey.Control, InputKey.F })) + migration.NewRealm.Remove(toggleFpsBind); + + break; + } } Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms"); diff --git a/osu.Game/Graphics/UserInterface/TernaryStateMenuItem.cs b/osu.Game/Graphics/UserInterface/TernaryStateMenuItem.cs index d2b6ff2dba..f98628a486 100644 --- a/osu.Game/Graphics/UserInterface/TernaryStateMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/TernaryStateMenuItem.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; namespace osu.Game.Graphics.UserInterface { @@ -20,7 +21,7 @@ namespace osu.Game.Graphics.UserInterface /// A function to inform what the next state should be when this item is clicked. /// The type of action which this performs. /// A delegate to be invoked when this is pressed. - protected TernaryStateMenuItem(string text, Func nextStateFunction, MenuItemType type = MenuItemType.Standard, Action action = null) + protected TernaryStateMenuItem(LocalisableString text, Func nextStateFunction, MenuItemType type = MenuItemType.Standard, Action action = null) : base(text, nextStateFunction, type, action) { } diff --git a/osu.Game/Graphics/UserInterface/TernaryStateRadioMenuItem.cs b/osu.Game/Graphics/UserInterface/TernaryStateRadioMenuItem.cs index 133362d3e6..30fea62cd7 100644 --- a/osu.Game/Graphics/UserInterface/TernaryStateRadioMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/TernaryStateRadioMenuItem.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using osu.Framework.Localisation; namespace osu.Game.Graphics.UserInterface { @@ -18,7 +19,7 @@ namespace osu.Game.Graphics.UserInterface /// The text to display. /// The type of action which this performs. /// A delegate to be invoked when this is pressed. - public TernaryStateRadioMenuItem(string text, MenuItemType type = MenuItemType.Standard, Action action = null) + public TernaryStateRadioMenuItem(LocalisableString text, MenuItemType type = MenuItemType.Standard, Action action = null) : base(text, getNextState, type, action) { } diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index aca0984e0f..02ede0a2f8 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -101,7 +101,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Alt, InputKey.Home }, GlobalAction.Home), - new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.F }, GlobalAction.ToggleFPSDisplay), + new KeyBinding(InputKey.None, GlobalAction.ToggleFPSDisplay), new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.ToggleToolbar), new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.S }, GlobalAction.ToggleSkinEditor), @@ -134,7 +134,8 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.EditorCloneSelection), new KeyBinding(new[] { InputKey.J }, GlobalAction.EditorNudgeLeft), new KeyBinding(new[] { InputKey.K }, GlobalAction.EditorNudgeRight), - new KeyBinding(new[] { InputKey.G }, GlobalAction.EditorCycleGridDisplayMode), + new KeyBinding(new[] { InputKey.G }, GlobalAction.EditorCycleGridSpacing), + new KeyBinding(new[] { InputKey.Shift, InputKey.G }, GlobalAction.EditorCycleGridType), new KeyBinding(new[] { InputKey.F5 }, GlobalAction.EditorTestGameplay), new KeyBinding(new[] { InputKey.T }, GlobalAction.EditorTapForBPM), new KeyBinding(new[] { InputKey.Control, InputKey.H }, GlobalAction.EditorFlipHorizontally), @@ -368,8 +369,8 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleChatFocus))] ToggleChatFocus, - [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCycleGridDisplayMode))] - EditorCycleGridDisplayMode, + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCycleGridSpacing))] + EditorCycleGridSpacing, [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestGameplay))] EditorTestGameplay, @@ -472,6 +473,9 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorSeekToNextSamplePoint))] EditorSeekToNextSamplePoint, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCycleGridType))] + EditorCycleGridType, } public enum GlobalActionCategory diff --git a/osu.Game/Localisation/DeleteConfirmationContentStrings.cs b/osu.Game/Localisation/DeleteConfirmationContentStrings.cs index d781fadbce..2b2f4dda54 100644 --- a/osu.Game/Localisation/DeleteConfirmationContentStrings.cs +++ b/osu.Game/Localisation/DeleteConfirmationContentStrings.cs @@ -19,6 +19,11 @@ namespace osu.Game.Localisation /// public static LocalisableString BeatmapVideos => new TranslatableString(getKey(@"beatmap_videos"), @"Are you sure you want to delete all beatmaps videos? This cannot be undone!"); + /// + /// "Are you sure you want to reset all local beatmap offsets? This cannot be undone!" + /// + public static LocalisableString Offsets => new TranslatableString(getKey(@"offsets"), @"Are you sure you want to reset all local beatmap offsets? This cannot be undone!"); + /// /// "Are you sure you want to delete all skins? This cannot be undone!" /// diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 206db1a166..ed80704a0a 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -190,9 +190,14 @@ namespace osu.Game.Localisation public static LocalisableString EditorCloneSelection => new TranslatableString(getKey(@"editor_clone_selection"), @"Clone selection"); /// - /// "Cycle grid display mode" + /// "Cycle grid spacing" /// - public static LocalisableString EditorCycleGridDisplayMode => new TranslatableString(getKey(@"editor_cycle_grid_display_mode"), @"Cycle grid display mode"); + public static LocalisableString EditorCycleGridSpacing => new TranslatableString(getKey(@"editor_cycle_grid_spacing"), @"Cycle grid spacing"); + + /// + /// "Cycle grid type" + /// + public static LocalisableString EditorCycleGridType => new TranslatableString(getKey(@"editor_cycle_grid_type"), @"Cycle grid type"); /// /// "Test gameplay" diff --git a/osu.Game/Localisation/MaintenanceSettingsStrings.cs b/osu.Game/Localisation/MaintenanceSettingsStrings.cs index 03e15e8393..6d5e0d5e0e 100644 --- a/osu.Game/Localisation/MaintenanceSettingsStrings.cs +++ b/osu.Game/Localisation/MaintenanceSettingsStrings.cs @@ -59,6 +59,11 @@ namespace osu.Game.Localisation /// public static LocalisableString DeleteAllBeatmapVideos => new TranslatableString(getKey(@"delete_all_beatmap_videos"), @"Delete ALL beatmap videos"); + /// + /// "Reset ALL beatmap offsets" + /// + public static LocalisableString ResetAllOffsets => new TranslatableString(getKey(@"reset_all_offsets"), @"Reset ALL beatmap offsets"); + /// /// "Delete ALL scores" /// diff --git a/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs b/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs index 33fda23cb0..b21446e18a 100644 --- a/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs +++ b/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs @@ -79,6 +79,11 @@ namespace osu.Game.Localisation.SkinComponents /// public static LocalisableString TextColourDescription => new TranslatableString(getKey(@"text_colour_description"), @"The colour of the text."); + /// + /// "Use relative size" + /// + public static LocalisableString UseRelativeSize => new TranslatableString(getKey(@"use_relative_size"), @"Use relative size"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/SkinEditorStrings.cs b/osu.Game/Localisation/SkinEditorStrings.cs index 3c1d1ff40d..d96ea7dd9f 100644 --- a/osu.Game/Localisation/SkinEditorStrings.cs +++ b/osu.Game/Localisation/SkinEditorStrings.cs @@ -49,6 +49,51 @@ namespace osu.Game.Localisation /// public static LocalisableString RevertToDefaultDescription => new TranslatableString(getKey(@"revert_to_default_description"), @"All layout elements for layers in the current screen will be reset to defaults."); + /// + /// "Closest" + /// + public static LocalisableString Closest => new TranslatableString(getKey(@"closest"), @"Closest"); + + /// + /// "Anchor" + /// + public static LocalisableString Anchor => new TranslatableString(getKey(@"anchor"), @"Anchor"); + + /// + /// "Origin" + /// + public static LocalisableString Origin => new TranslatableString(getKey(@"origin"), @"Origin"); + + /// + /// "Reset position" + /// + public static LocalisableString ResetPosition => new TranslatableString(getKey(@"reset_position"), @"Reset position"); + + /// + /// "Reset rotation" + /// + public static LocalisableString ResetRotation => new TranslatableString(getKey(@"reset_rotation"), @"Reset rotation"); + + /// + /// "Reset scale" + /// + public static LocalisableString ResetScale => new TranslatableString(getKey(@"reset_scale"), @"Reset scale"); + + /// + /// "Bring to front" + /// + public static LocalisableString BringToFront => new TranslatableString(getKey(@"bring_to_front"), @"Bring to front"); + + /// + /// "Send to back" + /// + public static LocalisableString SendToBack => new TranslatableString(getKey(@"send_to_back"), @"Send to back"); + + /// + /// "Current working layer" + /// + public static LocalisableString CurrentWorkingLayer => new TranslatableString(getKey(@"current_working_layer"), @"Current working layer"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Online/Multiplayer/InvalidPasswordException.cs b/osu.Game/Online/Multiplayer/InvalidPasswordException.cs index d3da8f491b..b76a1cc05d 100644 --- a/osu.Game/Online/Multiplayer/InvalidPasswordException.cs +++ b/osu.Game/Online/Multiplayer/InvalidPasswordException.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Runtime.Serialization; using Microsoft.AspNetCore.SignalR; namespace osu.Game.Online.Multiplayer @@ -10,13 +9,5 @@ namespace osu.Game.Online.Multiplayer [Serializable] public class InvalidPasswordException : HubException { - public InvalidPasswordException() - { - } - - protected InvalidPasswordException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/osu.Game/Online/Multiplayer/InvalidStateChangeException.cs b/osu.Game/Online/Multiplayer/InvalidStateChangeException.cs index 4c793dba68..2bae31196a 100644 --- a/osu.Game/Online/Multiplayer/InvalidStateChangeException.cs +++ b/osu.Game/Online/Multiplayer/InvalidStateChangeException.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Runtime.Serialization; using Microsoft.AspNetCore.SignalR; namespace osu.Game.Online.Multiplayer @@ -14,10 +13,5 @@ namespace osu.Game.Online.Multiplayer : base($"Cannot change from {oldState} to {newState}") { } - - protected InvalidStateChangeException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/osu.Game/Online/Multiplayer/InvalidStateException.cs b/osu.Game/Online/Multiplayer/InvalidStateException.cs index 27b111a781..c9705e9e53 100644 --- a/osu.Game/Online/Multiplayer/InvalidStateException.cs +++ b/osu.Game/Online/Multiplayer/InvalidStateException.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Runtime.Serialization; using Microsoft.AspNetCore.SignalR; namespace osu.Game.Online.Multiplayer @@ -14,10 +13,5 @@ namespace osu.Game.Online.Multiplayer : base(message) { } - - protected InvalidStateException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/osu.Game/Online/Multiplayer/NotHostException.cs b/osu.Game/Online/Multiplayer/NotHostException.cs index cd43b13e52..f4fd217c87 100644 --- a/osu.Game/Online/Multiplayer/NotHostException.cs +++ b/osu.Game/Online/Multiplayer/NotHostException.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Runtime.Serialization; using Microsoft.AspNetCore.SignalR; namespace osu.Game.Online.Multiplayer @@ -14,10 +13,5 @@ namespace osu.Game.Online.Multiplayer : base("User is attempting to perform a host level operation while not the host") { } - - protected NotHostException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/osu.Game/Online/Multiplayer/NotJoinedRoomException.cs b/osu.Game/Online/Multiplayer/NotJoinedRoomException.cs index 0a96406c16..72773e28db 100644 --- a/osu.Game/Online/Multiplayer/NotJoinedRoomException.cs +++ b/osu.Game/Online/Multiplayer/NotJoinedRoomException.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Runtime.Serialization; using Microsoft.AspNetCore.SignalR; namespace osu.Game.Online.Multiplayer @@ -14,10 +13,5 @@ namespace osu.Game.Online.Multiplayer : base("This user has not yet joined a multiplayer room.") { } - - protected NotJoinedRoomException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/osu.Game/Online/Multiplayer/UserBlockedException.cs b/osu.Game/Online/Multiplayer/UserBlockedException.cs index e964b13c75..58e86d9f32 100644 --- a/osu.Game/Online/Multiplayer/UserBlockedException.cs +++ b/osu.Game/Online/Multiplayer/UserBlockedException.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Runtime.Serialization; using Microsoft.AspNetCore.SignalR; namespace osu.Game.Online.Multiplayer @@ -16,10 +15,5 @@ namespace osu.Game.Online.Multiplayer : base(MESSAGE) { } - - protected UserBlockedException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/osu.Game/Online/Multiplayer/UserBlocksPMsException.cs b/osu.Game/Online/Multiplayer/UserBlocksPMsException.cs index 14ed6fc212..0ea583ae2c 100644 --- a/osu.Game/Online/Multiplayer/UserBlocksPMsException.cs +++ b/osu.Game/Online/Multiplayer/UserBlocksPMsException.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Runtime.Serialization; using Microsoft.AspNetCore.SignalR; namespace osu.Game.Online.Multiplayer @@ -16,10 +15,5 @@ namespace osu.Game.Online.Multiplayer : base(MESSAGE) { } - - protected UserBlocksPMsException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index a6868efb5d..c70c41feed 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -9,7 +9,6 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions; -using osu.Framework.Extensions.EnumExtensions; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -58,9 +57,11 @@ namespace osu.Game.Overlays.BeatmapSet.Scores } /// - /// The statistics that appear in the table, in order of appearance. + /// The names of the statistics that appear in the table. If multiple HitResults have the same + /// DisplayName (for example, "slider end" is the name for both and + /// in osu!) the name will only be listed once. /// - private readonly List<(HitResult result, LocalisableString displayName)> statisticResultTypes = new List<(HitResult, LocalisableString)>(); + private readonly List statisticResultNames = new List(); private bool showPerformancePoints; @@ -72,7 +73,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores return; showPerformancePoints = showPerformanceColumn; - statisticResultTypes.Clear(); + statisticResultNames.Clear(); for (int i = 0; i < scores.Count; i++) backgroundFlow.Add(new ScoreTableRowBackground(i, scores[i], row_height)); @@ -105,20 +106,18 @@ namespace osu.Game.Overlays.BeatmapSet.Scores var ruleset = scores.First().Ruleset.CreateInstance(); - foreach (var result in EnumExtensions.GetValuesInOrder()) + foreach (var resultGroup in ruleset.GetHitResults().GroupBy(r => r.displayName)) { - if (!allScoreStatistics.Contains(result)) + if (!resultGroup.Any(r => allScoreStatistics.Contains(r.result))) continue; // for the time being ignore bonus result types. // this is not being sent from the API and will be empty in all cases. - if (result.IsBonus()) + if (resultGroup.All(r => r.result.IsBonus())) continue; - var displayName = ruleset.GetDisplayNameForHitResult(result); - - columns.Add(new TableColumn(displayName, Anchor.CentreLeft, new Dimension(minSize: 35, maxSize: 60))); - statisticResultTypes.Add((result, displayName)); + columns.Add(new TableColumn(resultGroup.Key, Anchor.CentreLeft, new Dimension(minSize: 35, maxSize: 60))); + statisticResultNames.Add(resultGroup.Key); } if (showPerformancePoints) @@ -167,14 +166,25 @@ namespace osu.Game.Overlays.BeatmapSet.Scores #pragma warning restore 618 }; - var availableStatistics = score.GetStatisticsForDisplay().ToDictionary(tuple => tuple.Result); + var availableStatistics = score.GetStatisticsForDisplay().ToLookup(tuple => tuple.DisplayName); - foreach (var result in statisticResultTypes) + foreach (var columnName in statisticResultNames) { - if (!availableStatistics.TryGetValue(result.result, out var stat)) - stat = new HitResultDisplayStatistic(result.result, 0, null, result.displayName); + int count = 0; + int? maxCount = null; - content.Add(new StatisticText(stat.Count, stat.MaxCount, @"N0") { Colour = stat.Count == 0 ? Color4.Gray : Color4.White }); + if (availableStatistics.Contains(columnName)) + { + maxCount = 0; + + foreach (var s in availableStatistics[columnName]) + { + count += s.Count; + maxCount += s.MaxCount; + } + } + + content.Add(new StatisticText(count, maxCount, @"N0") { Colour = count == 0 ? Color4.Gray : Color4.White }); } if (showPerformancePoints) diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index 17704f63ee..e8833fa0a3 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -96,10 +96,17 @@ namespace osu.Game.Overlays.BeatmapSet.Scores } [BackgroundDependencyLoader] - private void load() + private void load(OsuColour colours) { if (score != null) + { totalScoreColumn.Current = scoreManager.GetBindableTotalScoreString(score); + + if (score.Accuracy == 1.0) accuracyColumn.TextColour = colours.GreenLight; +#pragma warning disable CS0618 + if (score.MaxCombo == score.BeatmapInfo!.MaxCombo) maxComboColumn.TextColour = colours.GreenLight; +#pragma warning restore CS0618 + } } private ScoreInfo score; @@ -228,6 +235,11 @@ namespace osu.Game.Overlays.BeatmapSet.Scores set => text.Text = value; } + public Colour4 TextColour + { + set => text.Colour = value; + } + public Drawable Drawable { set diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index a269dbdf4f..87920fdf55 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -376,11 +376,19 @@ namespace osu.Game.Overlays { Live result; - var possibleSets = getBeatmapSets().Where(s => !s.Value.Protected || allowProtectedTracks).ToArray(); + var possibleSets = getBeatmapSets().Where(s => !s.Value.Protected || allowProtectedTracks).ToList(); - if (possibleSets.Length == 0) + if (possibleSets.Count == 0) return null; + // if there is only one possible set left, play it, even if it is the same as the current track. + // looping is preferable over playing nothing. + if (possibleSets.Count == 1) + return possibleSets.Single(); + + // now that we actually know there is a choice, do not allow the current track to be played again. + possibleSets.RemoveAll(s => s.Value.Equals(current?.BeatmapSetInfo)); + // condition below checks if the signs of `randomHistoryDirection` and `direction` are opposite and not zero. // if that is the case, it means that the user had previously chosen next track `randomHistoryDirection` times and wants to go back, // or that the user had previously chosen previous track `randomHistoryDirection` times and wants to go forward. @@ -410,20 +418,20 @@ namespace osu.Game.Overlays switch (randomSelectAlgorithm.Value) { case RandomSelectAlgorithm.Random: - result = possibleSets[RNG.Next(possibleSets.Length)]; + result = possibleSets[RNG.Next(possibleSets.Count)]; break; case RandomSelectAlgorithm.RandomPermutation: - var notYetPlayedSets = possibleSets.Except(previousRandomSets).ToArray(); + var notYetPlayedSets = possibleSets.Except(previousRandomSets).ToList(); - if (notYetPlayedSets.Length == 0) + if (notYetPlayedSets.Count == 0) { notYetPlayedSets = possibleSets; previousRandomSets.Clear(); randomHistoryDirection = 0; } - result = notYetPlayedSets[RNG.Next(notYetPlayedSets.Length)]; + result = notYetPlayedSets[RNG.Next(notYetPlayedSets.Count)]; break; default: diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/BeatmapSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/BeatmapSettings.cs index d0a8fc7d2c..597e03fab2 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/BeatmapSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/BeatmapSettings.cs @@ -16,6 +16,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance private SettingsButton deleteBeatmapsButton = null!; private SettingsButton deleteBeatmapVideosButton = null!; + private SettingsButton resetOffsetsButton = null!; private SettingsButton restoreButton = null!; private SettingsButton undeleteButton = null!; @@ -47,6 +48,20 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance }, DeleteConfirmationContentStrings.BeatmapVideos)); } }); + + Add(resetOffsetsButton = new DangerousSettingsButton + { + Text = MaintenanceSettingsStrings.ResetAllOffsets, + Action = () => + { + dialogOverlay?.Push(new MassDeleteConfirmationDialog(() => + { + resetOffsetsButton.Enabled.Value = false; + Task.Run(beatmaps.ResetAllOffsets).ContinueWith(_ => Schedule(() => resetOffsetsButton.Enabled.Value = true)); + }, DeleteConfirmationContentStrings.Offsets)); + } + }); + AddRange(new Drawable[] { restoreButton = new SettingsButton diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 6f7781ee9c..42908f7102 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -361,7 +361,7 @@ namespace osu.Game.Overlays.SkinEditor componentsSidebar.Children = new[] { - new EditorSidebarSection("Current working layer") + new EditorSidebarSection(SkinEditorStrings.CurrentWorkingLayer) { Children = new Drawable[] { diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index 722ffd6d07..bc878b9214 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -13,6 +13,7 @@ using osu.Game.Extensions; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit.Compose.Components; +using osu.Game.Localisation; using osu.Game.Skinning; using osu.Game.Utils; using osuTK; @@ -101,19 +102,19 @@ namespace osu.Game.Overlays.SkinEditor protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) { - var closestItem = new TernaryStateRadioMenuItem("Closest", MenuItemType.Standard, _ => applyClosestAnchors()) + var closestItem = new TernaryStateRadioMenuItem(SkinEditorStrings.Closest, MenuItemType.Standard, _ => applyClosestAnchors()) { State = { Value = GetStateFromSelection(selection, c => !c.Item.UsesFixedAnchor) } }; - yield return new OsuMenuItem("Anchor") + yield return new OsuMenuItem(SkinEditorStrings.Anchor) { Items = createAnchorItems((d, a) => d.UsesFixedAnchor && ((Drawable)d).Anchor == a, applyFixedAnchors) .Prepend(closestItem) .ToArray() }; - yield return originMenu = new OsuMenuItem("Origin"); + yield return originMenu = new OsuMenuItem(SkinEditorStrings.Origin); closestItem.State.BindValueChanged(s => { @@ -125,19 +126,19 @@ namespace osu.Game.Overlays.SkinEditor yield return new OsuMenuItemSpacer(); - yield return new OsuMenuItem("Reset position", MenuItemType.Standard, () => + yield return new OsuMenuItem(SkinEditorStrings.ResetPosition, MenuItemType.Standard, () => { foreach (var blueprint in SelectedBlueprints) ((Drawable)blueprint.Item).Position = Vector2.Zero; }); - yield return new OsuMenuItem("Reset rotation", MenuItemType.Standard, () => + yield return new OsuMenuItem(SkinEditorStrings.ResetRotation, MenuItemType.Standard, () => { foreach (var blueprint in SelectedBlueprints) ((Drawable)blueprint.Item).Rotation = 0; }); - yield return new OsuMenuItem("Reset scale", MenuItemType.Standard, () => + yield return new OsuMenuItem(SkinEditorStrings.ResetScale, MenuItemType.Standard, () => { foreach (var blueprint in SelectedBlueprints) { @@ -153,9 +154,9 @@ namespace osu.Game.Overlays.SkinEditor yield return new OsuMenuItemSpacer(); - yield return new OsuMenuItem("Bring to front", MenuItemType.Standard, () => skinEditor.BringSelectionToFront()); + yield return new OsuMenuItem(SkinEditorStrings.BringToFront, MenuItemType.Standard, () => skinEditor.BringSelectionToFront()); - yield return new OsuMenuItem("Send to back", MenuItemType.Standard, () => skinEditor.SendSelectionToBack()); + yield return new OsuMenuItem(SkinEditorStrings.SendToBack, MenuItemType.Standard, () => skinEditor.SendSelectionToBack()); yield return new OsuMenuItemSpacer(); diff --git a/osu.Game/Rulesets/Mods/ModBarrelRoll.cs b/osu.Game/Rulesets/Mods/ModBarrelRoll.cs index 4f90496308..67f9da37be 100644 --- a/osu.Game/Rulesets/Mods/ModBarrelRoll.cs +++ b/osu.Game/Rulesets/Mods/ModBarrelRoll.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Mods private PlayfieldAdjustmentContainer playfieldAdjustmentContainer = null!; - public void Update(Playfield playfield) + public virtual void Update(Playfield playfield) { playfieldAdjustmentContainer.Rotation = CurrentRotation = (Direction.Value == RotationDirection.Counterclockwise ? -1 : 1) * 360 * (float)(playfield.Time.Current / 60000 * SpinSpeed.Value); } diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 92c18c9c1e..a3dabc7945 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -165,7 +165,7 @@ namespace osu.Game.Scoring } [UsedImplicitly] // Realm - private ScoreInfo() + protected ScoreInfo() { } diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index 71996718d9..44f021f93e 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -13,6 +13,7 @@ using osu.Framework.Layout; using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Configuration; +using osu.Game.Localisation.SkinComponents; using osu.Game.Rulesets.Judgements; using osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts; using osu.Game.Skinning; @@ -33,7 +34,7 @@ namespace osu.Game.Screens.Play.HUD Precision = 1 }; - [SettingSource("Use relative size")] + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.UseRelativeSize))] public BindableBool UseRelativeSize { get; } = new BindableBool(true); private ArgonHealthDisplayBar mainBar = null!; diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs index 92ac863e98..8dc5d60352 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs @@ -30,6 +30,9 @@ namespace osu.Game.Screens.Play.HUD [SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowTime), nameof(SongProgressStrings.ShowTimeDescription))] public Bindable ShowTime { get; } = new BindableBool(true); + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.UseRelativeSize))] + public BindableBool UseRelativeSize { get; } = new BindableBool(true); + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))] public BindableColour4 AccentColour { get; } = new BindableColour4(Colour4.White); @@ -99,6 +102,11 @@ namespace osu.Game.Screens.Play.HUD ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true); ShowTime.BindValueChanged(_ => info.FadeTo(ShowTime.Value ? 1 : 0, 200, Easing.In), true); AccentColour.BindValueChanged(_ => Colour = AccentColour.Value, true); + + // see comment in ArgonHealthDisplay.cs regarding RelativeSizeAxes + float previousWidth = Width; + UseRelativeSize.BindValueChanged(v => RelativeSizeAxes = v.NewValue ? Axes.X : Axes.None, true); + Width = previousWidth; } protected override void UpdateObjects(IEnumerable objects) diff --git a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs index 4e41901ee3..672017750d 100644 --- a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs @@ -37,6 +37,9 @@ namespace osu.Game.Screens.Play.HUD [SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowTime), nameof(SongProgressStrings.ShowTimeDescription))] public Bindable ShowTime { get; } = new BindableBool(true); + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.UseRelativeSize))] + public BindableBool UseRelativeSize { get; } = new BindableBool(true); + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))] public BindableColour4 AccentColour { get; } = new BindableColour4(Colour4.White); @@ -83,6 +86,11 @@ namespace osu.Game.Screens.Play.HUD private void load(OsuColour colours) { graph.FillColour = bar.FillColour = colours.BlueLighter; + + // see comment in ArgonHealthDisplay.cs regarding RelativeSizeAxes + float previousWidth = Width; + UseRelativeSize.BindValueChanged(v => RelativeSizeAxes = v.NewValue ? Axes.X : Axes.None, true); + Width = previousWidth; } protected override void LoadComplete() diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs index 89d083eca9..806985e19d 100644 --- a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs +++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs @@ -299,7 +299,7 @@ namespace osu.Game.Screens.Play.HUD { case GlobalAction.Back: if (!pendingAnimation) - BeginConfirm(); + Confirm(); return true; case GlobalAction.PauseGameplay: @@ -307,7 +307,7 @@ namespace osu.Game.Screens.Play.HUD if (ReplayLoaded.Value) return false; if (!pendingAnimation) - BeginConfirm(); + Confirm(); return true; } diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 7682bba9a6..9b4bf7d633 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -455,7 +455,19 @@ namespace osu.Game.Screens.Play MetadataInfo.Loading = true; content.FadeInFromZero(500, Easing.OutQuint); - content.ScaleTo(1, 650, Easing.OutQuint).Then().Schedule(prepareNewPlayer); + + if (quickRestart) + { + prepareNewPlayer(); + content.ScaleTo(1, 650, Easing.OutQuint); + } + else + { + content + .ScaleTo(1, 650, Easing.OutQuint) + .Then() + .Schedule(prepareNewPlayer); + } using (BeginDelayedSequence(delayBeforeSideDisplays)) { diff --git a/osu.Game/Screens/Select/BeatmapDeleteDialog.cs b/osu.Game/Screens/Select/BeatmapDeleteDialog.cs index 8bc40dbd9e..16f0cbe65e 100644 --- a/osu.Game/Screens/Select/BeatmapDeleteDialog.cs +++ b/osu.Game/Screens/Select/BeatmapDeleteDialog.cs @@ -14,7 +14,7 @@ namespace osu.Game.Screens.Select public BeatmapDeleteDialog(BeatmapSetInfo beatmapSet) { this.beatmapSet = beatmapSet; - BodyText = $@"{beatmapSet.Metadata.Artist} - {beatmapSet.Metadata.Title}"; + BodyText = beatmapSet.Metadata.GetDisplayTitleRomanisable(false); } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 8f38ae710c..3947cefb91 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -66,6 +66,8 @@ namespace osu.Game.Screens.Select.Carousel match &= !criteria.OverallDifficulty.HasFilter || criteria.OverallDifficulty.IsInRange(BeatmapInfo.Difficulty.OverallDifficulty); match &= !criteria.Length.HasFilter || criteria.Length.IsInRange(BeatmapInfo.Length); match &= !criteria.LastPlayed.HasFilter || criteria.LastPlayed.IsInRange(BeatmapInfo.LastPlayed ?? DateTimeOffset.MinValue); + match &= !criteria.DateRanked.HasFilter || (BeatmapInfo.BeatmapSet?.DateRanked != null && criteria.DateRanked.IsInRange(BeatmapInfo.BeatmapSet.DateRanked.Value)); + match &= !criteria.DateSubmitted.HasFilter || (BeatmapInfo.BeatmapSet?.DateSubmitted != null && criteria.DateSubmitted.IsInRange(BeatmapInfo.BeatmapSet.DateSubmitted.Value)); match &= !criteria.BPM.HasFilter || criteria.BPM.IsInRange(BeatmapInfo.BPM); match &= !criteria.BeatDivisor.HasFilter || criteria.BeatDivisor.IsInRange(BeatmapInfo.BeatDivisor); diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 190efd0fb0..77d7ff0e9f 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -37,6 +37,8 @@ namespace osu.Game.Screens.Select public OptionalRange BeatDivisor; public OptionalSet OnlineStatus = new OptionalSet(); public OptionalRange LastPlayed; + public OptionalRange DateRanked; + public OptionalRange DateSubmitted; public OptionalTextFilter Creator; public OptionalTextFilter Artist; public OptionalTextFilter Title; diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 6c9a95a250..ccffd34dc2 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -65,6 +65,12 @@ namespace osu.Game.Screens.Select case "lastplayed": return tryUpdateDateAgoRange(ref criteria.LastPlayed, op, value); + case "ranked": + return tryUpdateRankedDateRange(ref criteria.DateRanked, op, value); + + case "submitted": + return tryUpdateRankedDateRange(ref criteria.DateSubmitted, op, value); + case "played": if (!tryParseBool(value, out bool played)) return false; @@ -592,5 +598,163 @@ namespace osu.Game.Screens.Select return tryUpdateCriteriaRange(ref dateRange, op, dateTimeOffset.Value); } + + /// + /// Helper function for building a UTC date from only the year, month and day. + /// UTC is used to keep consistent search results with osu!web. + /// + private static DateTimeOffset dateTimeOffsetFromDateOnly(int year, int month, int day) => + new DateTimeOffset(year, month, day, 0, 0, 0, TimeSpan.Zero); + + /// + /// Parses a string containing a ranked or submitted date filter. + /// Returns a boolean depending on whether parsing was successful or not. + /// Accepted dates are in the formats `yyyy`, `yyyy-mm` and `yyyy-mm-dd`. + /// Leading zeros are accepted. Numbers can be separated by `-`, `/`, or `.` + /// + /// The to store the parsed data into, if successful. + /// The operator of the filtering query + /// The string value to attempt parsing for. + private static bool tryUpdateRankedDateRange(ref FilterCriteria.OptionalRange dateRange, Operator op, string val) + { + GroupCollection? match = tryMatchRegex(val, @"^(?\d+)([-/.](?\d+)([-/.](?\d+))?)?$"); + + if (match == null) + return false; + + int? year = null; + int? month = null; + int? day = null; + + List keys = new List { @"year", @"month", @"day" }; + + foreach (string key in keys) + { + if (!match.TryGetValue(key, out var group) || !group.Success) + continue; + + if (group.Success) + { + if (!tryParseDoubleWithPoint(group.Value, out double value)) + return false; + + switch (key) + { + case @"year": + year = (int)value; + break; + + case @"month": + month = (int)value; + break; + + case @"day": + day = (int)value; + break; + } + } + } + + if (year == null) + { + return false; + } + + try + { + DateTimeOffset dateTimeOffset; + + switch (op) + { + case Operator.Less: + month ??= 1; + day ??= 1; + + dateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value); + return tryUpdateCriteriaRange(ref dateRange, op, dateTimeOffset); + + case Operator.LessOrEqual: + if (month == null) + { + month = 1; + day = 1; + dateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddYears(1); + return tryUpdateCriteriaRange(ref dateRange, Operator.Less, dateTimeOffset); + } + + if (day == null) + { + day = 1; + dateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddMonths(1); + return tryUpdateCriteriaRange(ref dateRange, Operator.Less, dateTimeOffset); + } + + dateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddDays(1); + return tryUpdateCriteriaRange(ref dateRange, Operator.Less, dateTimeOffset); + + case Operator.GreaterOrEqual: + month ??= 1; + day ??= 1; + + dateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value); + return tryUpdateCriteriaRange(ref dateRange, op, dateTimeOffset); + + case Operator.Greater: + if (month == null) + { + month = 1; + day = 1; + dateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddYears(1); + return tryUpdateCriteriaRange(ref dateRange, Operator.GreaterOrEqual, dateTimeOffset); + } + + if (day == null) + { + day = 1; + dateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddMonths(1); + return tryUpdateCriteriaRange(ref dateRange, Operator.GreaterOrEqual, dateTimeOffset); + } + + dateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddDays(1); + return tryUpdateCriteriaRange(ref dateRange, Operator.GreaterOrEqual, dateTimeOffset); + + case Operator.Equal: + + DateTimeOffset minDateTimeOffset; + DateTimeOffset maxDateTimeOffset; + + if (month == null) + { + month = 1; + day = 1; + minDateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value); + maxDateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddYears(1); + return tryUpdateCriteriaRange(ref dateRange, Operator.GreaterOrEqual, minDateTimeOffset) + && tryUpdateCriteriaRange(ref dateRange, Operator.Less, maxDateTimeOffset); + } + + if (day == null) + { + day = 1; + minDateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value); + maxDateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddMonths(1); + return tryUpdateCriteriaRange(ref dateRange, Operator.GreaterOrEqual, minDateTimeOffset) + && tryUpdateCriteriaRange(ref dateRange, Operator.Less, maxDateTimeOffset); + } + + minDateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value); + maxDateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddDays(1); + return tryUpdateCriteriaRange(ref dateRange, Operator.GreaterOrEqual, minDateTimeOffset) + && tryUpdateCriteriaRange(ref dateRange, Operator.Less, maxDateTimeOffset); + + default: + return false; + } + } + catch (ArgumentOutOfRangeException) + { + return false; + } + } } } diff --git a/osu.Game/Skinning/Components/BeatmapAttributeText.cs b/osu.Game/Skinning/Components/BeatmapAttributeText.cs index 6e1d655cef..4ecf5acea7 100644 --- a/osu.Game/Skinning/Components/BeatmapAttributeText.cs +++ b/osu.Game/Skinning/Components/BeatmapAttributeText.cs @@ -3,8 +3,6 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -35,25 +33,6 @@ namespace osu.Game.Skinning.Components [Resolved] private IBindable beatmap { get; set; } = null!; - private readonly Dictionary valueDictionary = new Dictionary(); - - private static readonly ImmutableDictionary label_dictionary = new Dictionary - { - [BeatmapAttribute.CircleSize] = BeatmapsetsStrings.ShowStatsCs, - [BeatmapAttribute.Accuracy] = BeatmapsetsStrings.ShowStatsAccuracy, - [BeatmapAttribute.HPDrain] = BeatmapsetsStrings.ShowStatsDrain, - [BeatmapAttribute.ApproachRate] = BeatmapsetsStrings.ShowStatsAr, - [BeatmapAttribute.StarRating] = BeatmapsetsStrings.ShowStatsStars, - [BeatmapAttribute.Title] = EditorSetupStrings.Title, - [BeatmapAttribute.Artist] = EditorSetupStrings.Artist, - [BeatmapAttribute.DifficultyName] = EditorSetupStrings.DifficultyHeader, - [BeatmapAttribute.Creator] = EditorSetupStrings.Creator, - [BeatmapAttribute.Source] = EditorSetupStrings.Source, - [BeatmapAttribute.Length] = ArtistStrings.TracklistLength.ToTitle(), - [BeatmapAttribute.RankedStatus] = BeatmapDiscussionsStrings.IndexFormBeatmapsetStatusDefault, - [BeatmapAttribute.BPM] = BeatmapsetsStrings.ShowStatsBpm, - }.ToImmutableDictionary(); - private readonly OsuSpriteText text; public BeatmapAttributeText() @@ -74,52 +53,135 @@ namespace osu.Game.Skinning.Components { base.LoadComplete(); - Attribute.BindValueChanged(_ => updateLabel()); - Template.BindValueChanged(_ => updateLabel()); - beatmap.BindValueChanged(b => - { - updateBeatmapContent(b.NewValue); - updateLabel(); - }, true); + Attribute.BindValueChanged(_ => updateText()); + Template.BindValueChanged(_ => updateText()); + beatmap.BindValueChanged(_ => updateText()); + + updateText(); } - private void updateBeatmapContent(WorkingBeatmap workingBeatmap) - { - valueDictionary[BeatmapAttribute.Title] = new RomanisableString(workingBeatmap.BeatmapInfo.Metadata.TitleUnicode, workingBeatmap.BeatmapInfo.Metadata.Title); - valueDictionary[BeatmapAttribute.Artist] = new RomanisableString(workingBeatmap.BeatmapInfo.Metadata.ArtistUnicode, workingBeatmap.BeatmapInfo.Metadata.Artist); - valueDictionary[BeatmapAttribute.DifficultyName] = workingBeatmap.BeatmapInfo.DifficultyName; - valueDictionary[BeatmapAttribute.Creator] = workingBeatmap.BeatmapInfo.Metadata.Author.Username; - valueDictionary[BeatmapAttribute.Source] = workingBeatmap.BeatmapInfo.Metadata.Source; - valueDictionary[BeatmapAttribute.Length] = TimeSpan.FromMilliseconds(workingBeatmap.BeatmapInfo.Length).ToFormattedDuration(); - valueDictionary[BeatmapAttribute.RankedStatus] = workingBeatmap.BeatmapInfo.Status.GetLocalisableDescription(); - valueDictionary[BeatmapAttribute.BPM] = workingBeatmap.BeatmapInfo.BPM.ToLocalisableString(@"F2"); - valueDictionary[BeatmapAttribute.CircleSize] = ((double)workingBeatmap.BeatmapInfo.Difficulty.CircleSize).ToLocalisableString(@"F2"); - valueDictionary[BeatmapAttribute.HPDrain] = ((double)workingBeatmap.BeatmapInfo.Difficulty.DrainRate).ToLocalisableString(@"F2"); - valueDictionary[BeatmapAttribute.Accuracy] = ((double)workingBeatmap.BeatmapInfo.Difficulty.OverallDifficulty).ToLocalisableString(@"F2"); - valueDictionary[BeatmapAttribute.ApproachRate] = ((double)workingBeatmap.BeatmapInfo.Difficulty.ApproachRate).ToLocalisableString(@"F2"); - valueDictionary[BeatmapAttribute.StarRating] = workingBeatmap.BeatmapInfo.StarRating.ToLocalisableString(@"F2"); - } - - private void updateLabel() + private void updateText() { string numberedTemplate = Template.Value .Replace("{", "{{") .Replace("}", "}}") .Replace(@"{{Label}}", "{0}") - .Replace(@"{{Value}}", $"{{{1 + (int)Attribute.Value}}}"); + .Replace(@"{{Value}}", "{1}"); - object?[] args = valueDictionary.OrderBy(pair => pair.Key) - .Select(pair => pair.Value) - .Prepend(label_dictionary[Attribute.Value]) - .Cast() - .ToArray(); + List values = new List + { + getLabelString(Attribute.Value), + getValueString(Attribute.Value) + }; foreach (var type in Enum.GetValues()) { - numberedTemplate = numberedTemplate.Replace($"{{{{{type}}}}}", $"{{{1 + (int)type}}}"); + string replaced = numberedTemplate.Replace($@"{{{{{type}}}}}", $@"{{{values.Count}}}"); + + if (numberedTemplate != replaced) + { + numberedTemplate = replaced; + values.Add(getValueString(type)); + } } - text.Text = LocalisableString.Format(numberedTemplate, args); + text.Text = LocalisableString.Format(numberedTemplate, values.ToArray()); + } + + private LocalisableString getLabelString(BeatmapAttribute attribute) + { + switch (attribute) + { + case BeatmapAttribute.CircleSize: + return BeatmapsetsStrings.ShowStatsCs; + + case BeatmapAttribute.Accuracy: + return BeatmapsetsStrings.ShowStatsAccuracy; + + case BeatmapAttribute.HPDrain: + return BeatmapsetsStrings.ShowStatsDrain; + + case BeatmapAttribute.ApproachRate: + return BeatmapsetsStrings.ShowStatsAr; + + case BeatmapAttribute.StarRating: + return BeatmapsetsStrings.ShowStatsStars; + + case BeatmapAttribute.Title: + return EditorSetupStrings.Title; + + case BeatmapAttribute.Artist: + return EditorSetupStrings.Artist; + + case BeatmapAttribute.DifficultyName: + return EditorSetupStrings.DifficultyHeader; + + case BeatmapAttribute.Creator: + return EditorSetupStrings.Creator; + + case BeatmapAttribute.Source: + return EditorSetupStrings.Source; + + case BeatmapAttribute.Length: + return ArtistStrings.TracklistLength.ToTitle(); + + case BeatmapAttribute.RankedStatus: + return BeatmapDiscussionsStrings.IndexFormBeatmapsetStatusDefault; + + case BeatmapAttribute.BPM: + return BeatmapsetsStrings.ShowStatsBpm; + + default: + return string.Empty; + } + } + + private LocalisableString getValueString(BeatmapAttribute attribute) + { + switch (attribute) + { + case BeatmapAttribute.Title: + return new RomanisableString(beatmap.Value.BeatmapInfo.Metadata.TitleUnicode, beatmap.Value.BeatmapInfo.Metadata.Title); + + case BeatmapAttribute.Artist: + return new RomanisableString(beatmap.Value.BeatmapInfo.Metadata.ArtistUnicode, beatmap.Value.BeatmapInfo.Metadata.Artist); + + case BeatmapAttribute.DifficultyName: + return beatmap.Value.BeatmapInfo.DifficultyName; + + case BeatmapAttribute.Creator: + return beatmap.Value.BeatmapInfo.Metadata.Author.Username; + + case BeatmapAttribute.Source: + return beatmap.Value.BeatmapInfo.Metadata.Source; + + case BeatmapAttribute.Length: + return TimeSpan.FromMilliseconds(beatmap.Value.BeatmapInfo.Length).ToFormattedDuration(); + + case BeatmapAttribute.RankedStatus: + return beatmap.Value.BeatmapInfo.Status.GetLocalisableDescription(); + + case BeatmapAttribute.BPM: + return beatmap.Value.BeatmapInfo.BPM.ToLocalisableString(@"F2"); + + case BeatmapAttribute.CircleSize: + return ((double)beatmap.Value.BeatmapInfo.Difficulty.CircleSize).ToLocalisableString(@"F2"); + + case BeatmapAttribute.HPDrain: + return ((double)beatmap.Value.BeatmapInfo.Difficulty.DrainRate).ToLocalisableString(@"F2"); + + case BeatmapAttribute.Accuracy: + return ((double)beatmap.Value.BeatmapInfo.Difficulty.OverallDifficulty).ToLocalisableString(@"F2"); + + case BeatmapAttribute.ApproachRate: + return ((double)beatmap.Value.BeatmapInfo.Difficulty.ApproachRate).ToLocalisableString(@"F2"); + + case BeatmapAttribute.StarRating: + return beatmap.Value.BeatmapInfo.StarRating.ToLocalisableString(@"F2"); + + default: + return string.Empty; + } } protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40); diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index f982b11ad5..10c72ee2f1 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -18,16 +18,16 @@ - + - + - - - - - - + + + + + + @@ -37,11 +37,11 @@ - + - + - + diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 4a2ef97520..ccd6db354b 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -69,7 +69,7 @@ DO_NOT_SHOW HINT WARNING - WARNING + HINT WARNING WARNING DO_NOT_SHOW