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
-
+
-
+
-
-
-
-
-
-
+
+
+
+
+
+
@@ -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