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/Editor/TestSceneManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaSelectionHandler.cs
index 4285ef2029..a70b90789d 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaSelectionHandler.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaSelectionHandler.cs
@@ -118,5 +118,45 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
AddAssert("all objects in last column", () => EditorBeatmap.HitObjects.All(ho => ((ManiaHitObject)ho).Column == 3));
AddAssert("all objects remain selected", () => EditorBeatmap.SelectedHitObjects.SequenceEqual(EditorBeatmap.HitObjects));
}
+
+ [Test]
+ public void TestOffScreenObjectsRemainSelectedOnHorizontalFlip()
+ {
+ AddStep("create objects", () =>
+ {
+ for (int i = 0; i < 20; ++i)
+ EditorBeatmap.Add(new Note { StartTime = 1000 * i, Column = i % 4 });
+ });
+
+ AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
+ AddStep("flip", () =>
+ {
+ InputManager.PressKey(Key.ControlLeft);
+ InputManager.Key(Key.H);
+ InputManager.ReleaseKey(Key.ControlLeft);
+ });
+
+ AddAssert("all objects remain selected", () => EditorBeatmap.SelectedHitObjects.SequenceEqual(EditorBeatmap.HitObjects));
+ }
+
+ [Test]
+ public void TestOffScreenObjectsRemainSelectedOnVerticalFlip()
+ {
+ AddStep("create objects", () =>
+ {
+ for (int i = 0; i < 20; ++i)
+ EditorBeatmap.Add(new Note { StartTime = 1000 * i, Column = i % 4 });
+ });
+
+ AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
+ AddStep("flip", () =>
+ {
+ InputManager.PressKey(Key.ControlLeft);
+ InputManager.Key(Key.J);
+ InputManager.ReleaseKey(Key.ControlLeft);
+ });
+
+ AddAssert("all objects remain selected", () => EditorBeatmap.SelectedHitObjects.SequenceEqual(EditorBeatmap.HitObjects.Reverse()));
+ }
}
}
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.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
index 7e0991a4d4..74e616ac3f 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
@@ -54,9 +54,8 @@ namespace osu.Game.Rulesets.Mania.Edit
int firstColumn = flipOverOrigin ? 0 : selectedObjects.Min(ho => ho.Column);
int lastColumn = flipOverOrigin ? (int)EditorBeatmap.BeatmapInfo.Difficulty.CircleSize - 1 : selectedObjects.Max(ho => ho.Column);
- EditorBeatmap.PerformOnSelection(hitObject =>
+ performOnSelection(maniaObject =>
{
- var maniaObject = (ManiaHitObject)hitObject;
maniaPlayfield.Remove(maniaObject);
maniaObject.Column = firstColumn + (lastColumn - maniaObject.Column);
maniaPlayfield.Add(maniaObject);
@@ -71,7 +70,7 @@ namespace osu.Game.Rulesets.Mania.Edit
double selectionStartTime = selectedObjects.Min(ho => ho.StartTime);
double selectionEndTime = selectedObjects.Max(ho => ho.GetEndTime());
- EditorBeatmap.PerformOnSelection(hitObject =>
+ performOnSelection(hitObject =>
{
hitObject.StartTime = selectionStartTime + (selectionEndTime - hitObject.GetEndTime());
});
@@ -117,14 +116,21 @@ namespace osu.Game.Rulesets.Mania.Edit
columnDelta = Math.Clamp(columnDelta, -minColumn, maniaPlayfield.TotalColumns - 1 - maxColumn);
- EditorBeatmap.PerformOnSelection(h =>
+ performOnSelection(h =>
{
maniaPlayfield.Remove(h);
- ((ManiaHitObject)h).Column += columnDelta;
+ h.Column += columnDelta;
maniaPlayfield.Add(h);
});
+ }
- // `HitObjectUsageEventBuffer`'s usage transferal flows and the playfield's `SetKeepAlive()` functionality do not combine well with this operation's usage pattern,
+ private void performOnSelection(Action action)
+ {
+ var selectedObjects = EditorBeatmap.SelectedHitObjects.OfType().ToArray();
+
+ EditorBeatmap.PerformOnSelection(h => action.Invoke((ManiaHitObject)h));
+
+ // `HitObjectUsageEventBuffer`'s usage transferal flows and the playfield's `SetKeepAlive()` functionality do not combine well with mania's usage patterns,
// leading to selections being sometimes partially dropped if some of the objects being moved are off screen
// (check blame for detailed explanation).
// thus, ensure that selection is preserved manually.
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/Editor/TestSceneToolSwitching.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneToolSwitching.cs
new file mode 100644
index 0000000000..d5ab349a16
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneToolSwitching.cs
@@ -0,0 +1,55 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Tests.Visual;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Osu.Tests.Editor
+{
+ public partial class TestSceneToolSwitching : EditorTestScene
+ {
+ protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
+
+ [Test]
+ public void TestSliderAnchorMoveOperationEndsOnSwitchingTool()
+ {
+ var initialPosition = Vector2.Zero;
+
+ AddStep("store original anchor position", () => initialPosition = EditorBeatmap.HitObjects.OfType().First().Path.ControlPoints.ElementAt(1).Position);
+ AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.OfType().First()));
+ AddStep("move to second anchor", () => InputManager.MoveMouseTo(this.ChildrenOfType>().ElementAt(1)));
+ AddStep("start dragging", () => InputManager.PressButton(MouseButton.Left));
+ AddStep("drag away", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(0, -200)));
+ AddStep("switch tool", () => InputManager.PressButton(MouseButton.Button1));
+ AddStep("undo", () => Editor.Undo());
+ AddAssert("anchor back at original position",
+ () => EditorBeatmap.HitObjects.OfType().First().Path.ControlPoints.ElementAt(1).Position,
+ () => Is.EqualTo(initialPosition));
+ }
+
+ [Test]
+ public void TestSliderAnchorCreationOperationEndsOnSwitchingTool()
+ {
+ AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.OfType().First()));
+ AddStep("move to second anchor", () => InputManager.MoveMouseTo(this.ChildrenOfType>().ElementAt(1), new Vector2(-50, 0)));
+ AddStep("quick-create anchor", () =>
+ {
+ InputManager.PressKey(Key.ControlLeft);
+ InputManager.PressButton(MouseButton.Left);
+ InputManager.ReleaseKey(Key.ControlLeft);
+ });
+ AddStep("drag away", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(0, -200)));
+ AddStep("switch tool", () => InputManager.PressKey(Key.Number3));
+ AddStep("drag away further", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(0, -200)));
+ AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.OfType().First()));
+ AddStep("undo", () => Editor.Undo());
+ AddAssert("slider has three anchors again", () => EditorBeatmap.HitObjects.OfType().First().Path.ControlPoints, () => Has.Count.EqualTo(3));
+ }
+ }
+}
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/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index bca53dfa87..ddabf866ff 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -25,6 +25,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private int countMeh;
private int countMiss;
+ ///
+ /// Missed slider ticks that includes missed reverse arrows. Will only be correct on non-classic scores
+ ///
+ private int countSliderTickMiss;
+
+ ///
+ /// Amount of missed slider tails that don't break combo. Will only be correct on non-classic scores
+ ///
+ private int countSliderEndsDropped;
+
+ ///
+ /// Estimated total amount of combo breaks
+ ///
private double effectiveMissCount;
public OsuPerformanceCalculator()
@@ -44,7 +57,36 @@ namespace osu.Game.Rulesets.Osu.Difficulty
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
- effectiveMissCount = calculateEffectiveMissCount(osuAttributes);
+ countSliderEndsDropped = osuAttributes.SliderCount - score.Statistics.GetValueOrDefault(HitResult.SliderTailHit);
+ countSliderTickMiss = score.Statistics.GetValueOrDefault(HitResult.LargeTickMiss);
+
+ if (osuAttributes.SliderCount > 0)
+ {
+ if (usingClassicSliderAccuracy)
+ {
+ // Consider that full combo is maximum combo minus dropped slider tails since they don't contribute to combo but also don't break it
+ // In classic scores we can't know the amount of dropped sliders so we estimate to 10% of all sliders on the map
+ double fullComboThreshold = attributes.MaxCombo - 0.1 * osuAttributes.SliderCount;
+
+ if (scoreMaxCombo < fullComboThreshold)
+ effectiveMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
+
+ // In classic scores there can't be more misses than a sum of all non-perfect judgements
+ effectiveMissCount = Math.Min(effectiveMissCount, totalImperfectHits);
+ }
+ else
+ {
+ double fullComboThreshold = attributes.MaxCombo - countSliderEndsDropped;
+
+ if (scoreMaxCombo < fullComboThreshold)
+ effectiveMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
+
+ // Combine regular misses with tick misses since tick misses break combo as well
+ effectiveMissCount = Math.Min(effectiveMissCount, countSliderTickMiss + countMiss);
+ }
+ }
+
+ effectiveMissCount = Math.Max(countMiss, effectiveMissCount);
double multiplier = PERFORMANCE_BASE_MULTIPLIER;
@@ -124,8 +166,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (attributes.SliderCount > 0)
{
- double estimateSliderEndsDropped = Math.Clamp(Math.Min(countOk + countMeh + countMiss, attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders);
- double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateSliderEndsDropped / estimateDifficultSliders, 3) + attributes.SliderFactor;
+ double estimateImproperlyFollowedDifficultSliders;
+
+ if (usingClassicSliderAccuracy)
+ {
+ // When the score is considered classic (regardless if it was made on old client or not) we consider all missing combo to be dropped difficult sliders
+ int maximumPossibleDroppedSliders = totalImperfectHits;
+ estimateImproperlyFollowedDifficultSliders = Math.Clamp(Math.Min(maximumPossibleDroppedSliders, attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders);
+ }
+ else
+ {
+ // We add tick misses here since they too mean that the player didn't follow the slider properly
+ // We however aren't adding misses here because missing slider heads has a harsh penalty by itself and doesn't mean that the rest of the slider wasn't followed properly
+ estimateImproperlyFollowedDifficultSliders = Math.Min(countSliderEndsDropped + countSliderTickMiss, estimateDifficultSliders);
+ }
+
+ double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateImproperlyFollowedDifficultSliders / estimateDifficultSliders, 3) + attributes.SliderFactor;
aimValue *= sliderNerfFactor;
}
@@ -247,29 +303,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return flashlightValue;
}
- private double calculateEffectiveMissCount(OsuDifficultyAttributes attributes)
- {
- // Guess the number of misses + slider breaks from combo
- double comboBasedMissCount = 0.0;
-
- if (attributes.SliderCount > 0)
- {
- double fullComboThreshold = attributes.MaxCombo - 0.1 * attributes.SliderCount;
- if (scoreMaxCombo < fullComboThreshold)
- comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
- }
-
- // Clamp miss count to maximum amount of possible breaks
- comboBasedMissCount = Math.Min(comboBasedMissCount, countOk + countMeh + countMiss);
-
- return Math.Max(countMiss, comboBasedMissCount);
- }
-
// Miss penalty assumes that a player will miss on the hardest parts of a map,
// so we use the amount of relatively difficult sections to adjust miss penalty
// to make it more punishing on maps with lower amount of hard sections.
private double calculateMissPenalty(double missCount, double difficultStrainCount) => 0.96 / ((missCount / (4 * Math.Pow(Math.Log(difficultStrainCount), 0.94))) + 1);
private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0);
private int totalHits => countGreat + countOk + countMeh + countMiss;
+ private int totalImperfectHits => countOk + countMeh + countMiss;
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
index 70ccbdfdc4..f114516300 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
@@ -333,6 +333,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
base.Dispose(isDisposing);
foreach (var p in Pieces)
p.ControlPoint.Changed -= controlPointChanged;
+
+ if (draggedControlPointIndex >= 0)
+ DragEnded();
}
private void selectionRequested(PathControlPointPiece piece, MouseButtonEvent e)
@@ -392,7 +395,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private Vector2[] dragStartPositions;
private PathType?[] dragPathTypes;
- private int draggedControlPointIndex;
+ private int draggedControlPointIndex = -1;
private HashSet selectedControlPoints;
private List
-
+
-
+
-
-
-
-
-
-
+
+
+
+
+
+
@@ -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