diff --git a/.run/Dual client test.run.xml b/.run/Dual client test.run.xml
new file mode 100644
index 0000000000..e112aa3d5d
--- /dev/null
+++ b/.run/Dual client test.run.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/osu! (Second Client).run.xml b/.run/osu! (Second Client).run.xml
new file mode 100644
index 0000000000..599b4b986b
--- /dev/null
+++ b/.run/osu! (Second Client).run.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/osu.Android.props b/osu.Android.props
index 171a0862a1..69a89c3cd0 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,8 +51,8 @@
-
-
+
+
diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs
index 5fb09c0cef..cbee1694ba 100644
--- a/osu.Desktop/Program.cs
+++ b/osu.Desktop/Program.cs
@@ -3,7 +3,6 @@
using System;
using System.IO;
-using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework;
@@ -17,13 +16,43 @@ namespace osu.Desktop
{
public static class Program
{
+ private const string base_game_name = @"osu";
+
[STAThread]
public static int Main(string[] args)
{
// Back up the cwd before DesktopGameHost changes it
var cwd = Environment.CurrentDirectory;
- using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true))
+ string gameName = base_game_name;
+ bool tournamentClient = false;
+
+ foreach (var arg in args)
+ {
+ var split = arg.Split('=');
+
+ var key = split[0];
+ var val = split[1];
+
+ switch (key)
+ {
+ case "--tournament":
+ tournamentClient = true;
+ break;
+
+ case "--debug-client-id":
+ if (!DebugUtils.IsDebugBuild)
+ throw new InvalidOperationException("Cannot use this argument in a non-debug build.");
+
+ if (!int.TryParse(val, out int clientID))
+ throw new ArgumentException("Provided client ID must be an integer.");
+
+ gameName = $"{base_game_name}-{clientID}";
+ break;
+ }
+ }
+
+ using (DesktopGameHost host = Host.GetSuitableHost(gameName, true))
{
host.ExceptionThrown += handleException;
@@ -48,16 +77,10 @@ namespace osu.Desktop
return 0;
}
- switch (args.FirstOrDefault() ?? string.Empty)
- {
- default:
- host.Run(new OsuGameDesktop(args));
- break;
-
- case "--tournament":
- host.Run(new TournamentGame());
- break;
- }
+ if (tournamentClient)
+ host.Run(new TournamentGame());
+ else
+ host.Run(new OsuGameDesktop(args));
return 0;
}
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs
index dcdc32145b..a458771550 100644
--- a/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs
@@ -1,10 +1,17 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Catch.Beatmaps;
+using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Screens.Edit;
using osu.Game.Tests.Visual;
+using osuTK;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
@@ -14,11 +21,52 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
protected override Container Content => contentContainer;
+ [Cached(typeof(EditorBeatmap))]
+ [Cached(typeof(IBeatSnapProvider))]
+ protected readonly EditorBeatmap EditorBeatmap;
+
private readonly CatchEditorTestSceneContainer contentContainer;
protected CatchSelectionBlueprintTestScene()
{
- base.Content.Add(contentContainer = new CatchEditorTestSceneContainer());
+ EditorBeatmap = new EditorBeatmap(new CatchBeatmap());
+ EditorBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = 0;
+ EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint
+ {
+ BeatLength = 100
+ });
+
+ base.Content.Add(new EditorBeatmapDependencyContainer(EditorBeatmap, new BindableBeatDivisor())
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ EditorBeatmap,
+ contentContainer = new CatchEditorTestSceneContainer()
+ },
+ });
+ }
+
+ protected void AddMouseMoveStep(double time, float x) => AddStep($"move to time={time}, x={x}", () =>
+ {
+ float y = HitObjectContainer.PositionAtTime(time);
+ Vector2 pos = HitObjectContainer.ToScreenSpace(new Vector2(x, y + HitObjectContainer.DrawHeight));
+ InputManager.MoveMouseTo(pos);
+ });
+
+ private class EditorBeatmapDependencyContainer : Container
+ {
+ [Cached]
+ private readonly EditorClock editorClock;
+
+ [Cached]
+ private readonly BindableBeatDivisor beatDivisor;
+
+ public EditorBeatmapDependencyContainer(IBeatmap beatmap, BindableBeatDivisor beatDivisor)
+ {
+ editorClock = new EditorClock(beatmap, beatDivisor);
+ this.beatDivisor = beatDivisor;
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs
index 1b96175020..f5ef5c5e18 100644
--- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs
@@ -1,38 +1,286 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Game.Beatmaps;
-using osu.Game.Beatmaps.ControlPoints;
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Framework.Timing;
+using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
+using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
+using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public class TestSceneJuiceStreamSelectionBlueprint : CatchSelectionBlueprintTestScene
{
- public TestSceneJuiceStreamSelectionBlueprint()
+ private JuiceStream hitObject;
+
+ private readonly ManualClock manualClock = new ManualClock();
+
+ [SetUp]
+ public void SetUp() => Schedule(() =>
{
- var hitObject = new JuiceStream
+ EditorBeatmap.Clear();
+ Content.Clear();
+
+ manualClock.CurrentTime = 0;
+ Content.Clock = new FramedClock(manualClock);
+
+ InputManager.ReleaseButton(MouseButton.Left);
+ InputManager.ReleaseKey(Key.ShiftLeft);
+ InputManager.ReleaseKey(Key.ControlLeft);
+ });
+
+ [Test]
+ public void TestBasicComponentLayout()
+ {
+ double[] times = { 100, 300, 500 };
+ float[] positions = { 100, 200, 100 };
+ addBlueprintStep(times, positions);
+
+ for (int i = 0; i < times.Length; i++)
+ addVertexCheckStep(times.Length, i, times[i], positions[i]);
+
+ AddAssert("correct outline count", () =>
{
- OriginalX = 100,
- StartTime = 100,
- Path = new SliderPath(PathType.PerfectCurve, new[]
+ var expected = hitObject.NestedHitObjects.Count(h => !(h is TinyDroplet));
+ return this.ChildrenOfType().Count() == expected;
+ });
+ AddAssert("correct vertex piece count", () =>
+ this.ChildrenOfType().Count() == times.Length);
+
+ AddAssert("first vertex is semitransparent", () =>
+ Precision.DefinitelyBigger(1, this.ChildrenOfType().First().Alpha));
+ }
+
+ [Test]
+ public void TestVertexDrag()
+ {
+ double[] times = { 100, 400, 700 };
+ float[] positions = { 100, 100, 100 };
+ addBlueprintStep(times, positions);
+
+ addDragStartStep(times[1], positions[1]);
+
+ AddMouseMoveStep(500, 150);
+ addVertexCheckStep(3, 1, 500, 150);
+
+ addDragEndStep();
+ addDragStartStep(times[2], positions[2]);
+
+ AddMouseMoveStep(300, 50);
+ addVertexCheckStep(3, 1, 300, 50);
+ addVertexCheckStep(3, 2, 500, 150);
+
+ AddMouseMoveStep(-100, 100);
+ addVertexCheckStep(3, 1, times[0], positions[0]);
+ }
+
+ [Test]
+ public void TestMultipleDrag()
+ {
+ double[] times = { 100, 300, 500, 700 };
+ float[] positions = { 100, 100, 100, 100 };
+ addBlueprintStep(times, positions);
+
+ AddMouseMoveStep(times[1], positions[1]);
+ AddStep("press left", () => InputManager.PressButton(MouseButton.Left));
+ AddStep("release left", () => InputManager.ReleaseButton(MouseButton.Left));
+ AddStep("hold control", () => InputManager.PressKey(Key.ControlLeft));
+ addDragStartStep(times[2], positions[2]);
+
+ AddMouseMoveStep(times[2] - 50, positions[2] - 50);
+ addVertexCheckStep(4, 1, times[1] - 50, positions[1] - 50);
+ addVertexCheckStep(4, 2, times[2] - 50, positions[2] - 50);
+ }
+
+ [Test]
+ public void TestClampedPositionIsRestored()
+ {
+ const double velocity = 0.25;
+ double[] times = { 100, 500, 700 };
+ float[] positions = { 100, 100, 100 };
+ addBlueprintStep(times, positions, velocity);
+
+ addDragStartStep(times[1], positions[1]);
+
+ AddMouseMoveStep(times[1], 200);
+ addVertexCheckStep(3, 1, times[1], 200);
+ addVertexCheckStep(3, 2, times[2], 150);
+
+ AddMouseMoveStep(times[1], 100);
+ addVertexCheckStep(3, 1, times[1], 100);
+ // Stored position is restored.
+ addVertexCheckStep(3, 2, times[2], positions[2]);
+
+ AddMouseMoveStep(times[1], 300);
+ addDragEndStep();
+ addDragStartStep(times[1], 300);
+
+ AddMouseMoveStep(times[1], 100);
+ // Position is different because a changed position is committed when the previous drag is ended.
+ addVertexCheckStep(3, 2, times[2], 250);
+ }
+
+ [Test]
+ public void TestScrollWhileDrag()
+ {
+ double[] times = { 300, 500 };
+ float[] positions = { 100, 100 };
+ addBlueprintStep(times, positions);
+
+ addDragStartStep(times[1], positions[1]);
+ // This mouse move is necessary to start drag and capture the input.
+ AddMouseMoveStep(times[1], positions[1] + 50);
+
+ AddStep("scroll playfield", () => manualClock.CurrentTime += 200);
+ AddMouseMoveStep(times[1] + 200, positions[1] + 100);
+ addVertexCheckStep(2, 1, times[1] + 200, positions[1] + 100);
+ }
+
+ [Test]
+ public void TestUpdateFromHitObject()
+ {
+ double[] times = { 100, 300 };
+ float[] positions = { 200, 200 };
+ addBlueprintStep(times, positions);
+
+ AddStep("update hit object path", () =>
+ {
+ hitObject.Path = new SliderPath(PathType.PerfectCurve, new[]
{
Vector2.Zero,
- new Vector2(200, 100),
+ new Vector2(100, 100),
new Vector2(0, 200),
- }),
- };
- var controlPoint = new ControlPointInfo();
- controlPoint.Add(0, new TimingControlPoint
- {
- BeatLength = 100
+ });
+ EditorBeatmap.Update(hitObject);
});
- hitObject.ApplyDefaults(controlPoint, new BeatmapDifficulty { CircleSize = 0 });
+ AddAssert("path is updated", () => getVertices().Count > 2);
+ }
+
+ [Test]
+ public void TestAddVertex()
+ {
+ double[] times = { 100, 700 };
+ float[] positions = { 200, 200 };
+ addBlueprintStep(times, positions, 0.2);
+
+ addAddVertexSteps(500, 150);
+ addVertexCheckStep(3, 1, 500, 150);
+
+ addAddVertexSteps(90, 220);
+ addVertexCheckStep(4, 1, times[0], positions[0]);
+
+ addAddVertexSteps(750, 180);
+ addVertexCheckStep(5, 4, 750, 180);
+ AddAssert("duration is changed", () => Precision.AlmostEquals(hitObject.Duration, 800 - times[0], 1e-3));
+ }
+
+ [Test]
+ public void TestDeleteVertex()
+ {
+ double[] times = { 100, 300, 500 };
+ float[] positions = { 100, 200, 150 };
+ addBlueprintStep(times, positions);
+
+ addDeleteVertexSteps(times[1], positions[1]);
+ addVertexCheckStep(2, 1, times[2], positions[2]);
+
+ // The first vertex cannot be deleted.
+ addDeleteVertexSteps(times[0], positions[0]);
+ addVertexCheckStep(2, 0, times[0], positions[0]);
+
+ addDeleteVertexSteps(times[2], positions[2]);
+ addVertexCheckStep(1, 0, times[0], positions[0]);
+ }
+
+ [Test]
+ public void TestVertexResampling()
+ {
+ addBlueprintStep(100, 100, new SliderPath(PathType.PerfectCurve, new[]
+ {
+ Vector2.Zero,
+ new Vector2(100, 100),
+ new Vector2(50, 200),
+ }), 0.5);
+ AddAssert("1 vertex per 1 nested HO", () => getVertices().Count == hitObject.NestedHitObjects.Count);
+ AddAssert("slider path not yet changed", () => hitObject.Path.ControlPoints[0].Type.Value == PathType.PerfectCurve);
+ addAddVertexSteps(150, 150);
+ AddAssert("slider path change to linear", () => hitObject.Path.ControlPoints[0].Type.Value == PathType.Linear);
+ }
+
+ private void addBlueprintStep(double time, float x, SliderPath sliderPath, double velocity) => AddStep("add selection blueprint", () =>
+ {
+ hitObject = new JuiceStream
+ {
+ StartTime = time,
+ X = x,
+ Path = sliderPath,
+ };
+ EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = velocity;
+ EditorBeatmap.Add(hitObject);
+ EditorBeatmap.Update(hitObject);
+ Assert.That(hitObject.Velocity, Is.EqualTo(velocity));
AddBlueprint(new JuiceStreamSelectionBlueprint(hitObject));
+ });
+
+ private void addBlueprintStep(double[] times, float[] positions, double velocity = 0.5)
+ {
+ var path = new JuiceStreamPath();
+ for (int i = 1; i < times.Length; i++)
+ path.Add((times[i] - times[0]) * velocity, positions[i] - positions[0]);
+
+ var sliderPath = new SliderPath();
+ path.ConvertToSliderPath(sliderPath, 0);
+ addBlueprintStep(times[0], positions[0], sliderPath, velocity);
+ }
+
+ private IReadOnlyList getVertices() => this.ChildrenOfType().Single().Vertices;
+
+ private void addVertexCheckStep(int count, int index, double time, float x) => AddAssert($"vertex {index} of {count} at {time}, {x}", () =>
+ {
+ double expectedDistance = (time - hitObject.StartTime) * hitObject.Velocity;
+ float expectedX = x - hitObject.OriginalX;
+ var vertices = getVertices();
+ return vertices.Count == count &&
+ Precision.AlmostEquals(vertices[index].Distance, expectedDistance, 1e-3) &&
+ Precision.AlmostEquals(vertices[index].X, expectedX);
+ });
+
+ private void addDragStartStep(double time, float x)
+ {
+ AddMouseMoveStep(time, x);
+ AddStep("start dragging", () => InputManager.PressButton(MouseButton.Left));
+ }
+
+ private void addDragEndStep() => AddStep("end dragging", () => InputManager.ReleaseButton(MouseButton.Left));
+
+ private void addAddVertexSteps(double time, float x)
+ {
+ AddMouseMoveStep(time, x);
+ AddStep("add vertex", () =>
+ {
+ InputManager.PressKey(Key.ControlLeft);
+ InputManager.Click(MouseButton.Left);
+ InputManager.ReleaseKey(Key.ControlLeft);
+ });
+ }
+
+ private void addDeleteVertexSteps(double time, float x)
+ {
+ AddMouseMoveStep(time, x);
+ AddStep("delete vertex", () =>
+ {
+ InputManager.PressKey(Key.ShiftLeft);
+ InputManager.Click(MouseButton.Left);
+ InputManager.ReleaseKey(Key.ShiftLeft);
+ });
}
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs
index ec186bcfb2..83f28086e6 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs
@@ -3,7 +3,6 @@
using System.Linq;
using NUnit.Framework;
-using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -24,16 +23,12 @@ namespace osu.Game.Rulesets.Catch.Tests
{
public class TestSceneCatchSkinConfiguration : OsuTestScene
{
- [Cached]
- private readonly DroppedObjectContainer droppedObjectContainer;
-
private Catcher catcher;
private readonly Container container;
public TestSceneCatchSkinConfiguration()
{
- Add(droppedObjectContainer = new DroppedObjectContainer());
Add(container = new Container { RelativeSizeAxes = Axes.Both });
}
@@ -46,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.Tests
var skin = new TestSkin { FlipCatcherPlate = flip };
container.Child = new SkinProvidingContainer(skin)
{
- Child = catcher = new Catcher(new Container())
+ Child = catcher = new Catcher(new Container(), new DroppedObjectContainer())
{
Anchor = Anchor.Centre
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
index 0a2dff6a21..b4282e6784 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
@@ -31,10 +31,12 @@ namespace osu.Game.Rulesets.Catch.Tests
[Resolved]
private OsuConfigManager config { get; set; }
- private TestCatcher catcher;
+ private Container trailContainer;
private DroppedObjectContainer droppedObjectContainer;
+ private TestCatcher catcher;
+
[SetUp]
public void SetUp() => Schedule(() =>
{
@@ -43,24 +45,18 @@ namespace osu.Game.Rulesets.Catch.Tests
CircleSize = 0,
};
- var trailContainer = new Container
+ trailContainer = new Container();
+ droppedObjectContainer = new DroppedObjectContainer();
+
+ Child = new Container
{
Anchor = Anchor.Centre,
- };
- droppedObjectContainer = new DroppedObjectContainer();
- Child = new DependencyProvidingContainer
- {
- CachedDependencies = new (Type, object)[]
- {
- (typeof(DroppedObjectContainer), droppedObjectContainer),
- },
Children = new Drawable[]
{
droppedObjectContainer,
- catcher = new TestCatcher(trailContainer, difficulty),
- trailContainer
- },
- Anchor = Anchor.Centre
+ catcher = new TestCatcher(trailContainer, droppedObjectContainer, difficulty),
+ trailContainer,
+ }
};
});
@@ -298,8 +294,8 @@ namespace osu.Game.Rulesets.Catch.Tests
{
public IEnumerable CaughtObjects => this.ChildrenOfType();
- public TestCatcher(Container trailsTarget, BeatmapDifficulty difficulty)
- : base(trailsTarget, difficulty)
+ public TestCatcher(Container trailsTarget, DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty)
+ : base(trailsTarget, droppedObjectTarget, difficulty)
{
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
index 877e115e2f..6a518cf0ef 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
@@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{
area.OnNewResult(drawable, new CatchJudgementResult(fruit, new CatchJudgement())
{
- Type = area.MovableCatcher.CanCatch(fruit) ? HitResult.Great : HitResult.Miss
+ Type = area.Catcher.CanCatch(fruit) ? HitResult.Great : HitResult.Miss
});
drawable.Expire();
@@ -119,16 +119,19 @@ namespace osu.Game.Rulesets.Catch.Tests
private class TestCatcherArea : CatcherArea
{
- [Cached]
- private readonly DroppedObjectContainer droppedObjectContainer;
-
public TestCatcherArea(BeatmapDifficulty beatmapDifficulty)
- : base(beatmapDifficulty)
{
- AddInternal(droppedObjectContainer = new DroppedObjectContainer());
+ var droppedObjectContainer = new DroppedObjectContainer();
+
+ Add(droppedObjectContainer);
+
+ Catcher = new Catcher(this, droppedObjectContainer, beatmapDifficulty)
+ {
+ X = CatchPlayfield.CENTER_X
+ };
}
- public void ToggleHyperDash(bool status) => MovableCatcher.SetHyperDashState(status ? 2 : 1);
+ public void ToggleHyperDash(bool status) => Catcher.SetHyperDashState(status ? 2 : 1);
}
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
index fd6a9c7b7b..e7c7dc3c98 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
@@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Catch.Tests
private bool playfieldIsEmpty => !((CatchPlayfield)drawableRuleset.Playfield).AllHitObjects.Any(h => h.IsAlive);
- private CatcherAnimationState catcherState => ((CatchPlayfield)drawableRuleset.Playfield).CatcherArea.MovableCatcher.CurrentState;
+ private CatcherAnimationState catcherState => ((CatchPlayfield)drawableRuleset.Playfield).Catcher.CurrentState;
private void spawnFruits(bool hit = false)
{
@@ -162,7 +162,7 @@ namespace osu.Game.Rulesets.Catch.Tests
float xCoords = CatchPlayfield.CENTER_X;
if (drawableRuleset.Playfield is CatchPlayfield catchPlayfield)
- catchPlayfield.CatcherArea.MovableCatcher.X = xCoords - x_offset;
+ catchPlayfield.Catcher.X = xCoords - x_offset;
if (hit)
xCoords -= x_offset;
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
index db09b2bc6b..163fee49fb 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.Tests
// this needs to be done within the frame stable context due to how quickly hyperdash state changes occur.
Player.DrawableRuleset.FrameStableComponents.OnUpdate += d =>
{
- var catcher = Player.ChildrenOfType().FirstOrDefault()?.MovableCatcher;
+ var catcher = Player.ChildrenOfType().FirstOrDefault();
if (catcher == null)
return;
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
index e7b0259ea2..73797d0a6a 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
@@ -113,36 +113,45 @@ namespace osu.Game.Rulesets.Catch.Tests
private void checkHyperDashCatcherColour(ISkin skin, Color4 expectedCatcherColour, Color4? expectedEndGlowColour = null)
{
- CatcherArea catcherArea = null;
+ Container trailsContainer = null;
+ Catcher catcher = null;
CatcherTrailDisplay trails = null;
AddStep("create hyper-dashing catcher", () =>
{
- Child = setupSkinHierarchy(catcherArea = new TestCatcherArea
+ trailsContainer = new Container();
+ Child = setupSkinHierarchy(new Container
{
Anchor = Anchor.Centre,
- Origin = Anchor.Centre
+ Children = new Drawable[]
+ {
+ catcher = new Catcher(trailsContainer, new DroppedObjectContainer())
+ {
+ Scale = new Vector2(4)
+ },
+ trailsContainer
+ }
}, skin);
});
AddStep("get trails container", () =>
{
- trails = catcherArea.OfType().Single();
- catcherArea.MovableCatcher.SetHyperDashState(2);
+ trails = trailsContainer.OfType().Single();
+ catcher.SetHyperDashState(2);
});
- AddUntilStep("catcher colour is correct", () => catcherArea.MovableCatcher.Colour == expectedCatcherColour);
+ AddUntilStep("catcher colour is correct", () => catcher.Colour == expectedCatcherColour);
AddAssert("catcher trails colours are correct", () => trails.HyperDashTrailsColour == expectedCatcherColour);
AddAssert("catcher end-glow colours are correct", () => trails.EndGlowSpritesColour == (expectedEndGlowColour ?? expectedCatcherColour));
AddStep("finish hyper-dashing", () =>
{
- catcherArea.MovableCatcher.SetHyperDashState();
- catcherArea.MovableCatcher.FinishTransforms();
+ catcher.SetHyperDashState();
+ catcher.FinishTransforms();
});
- AddAssert("catcher colour returned to white", () => catcherArea.MovableCatcher.Colour == Color4.White);
+ AddAssert("catcher colour returned to white", () => catcher.Colour == Color4.White);
}
private void checkHyperDashFruitColour(ISkin skin, Color4 expectedColour)
@@ -205,18 +214,5 @@ namespace osu.Game.Rulesets.Catch.Tests
{
}
}
-
- private class TestCatcherArea : CatcherArea
- {
- [Cached]
- private readonly DroppedObjectContainer droppedObjectContainer;
-
- public TestCatcherArea()
- {
- Scale = new Vector2(4f);
-
- AddInternal(droppedObjectContainer = new DroppedObjectContainer());
- }
- }
}
}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs
new file mode 100644
index 0000000000..8aaeef045f
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs
@@ -0,0 +1,190 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using JetBrains.Annotations;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Primitives;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
+{
+ public abstract class EditablePath : CompositeDrawable
+ {
+ public int PathId => path.InvalidationID;
+
+ public IReadOnlyList Vertices => path.Vertices;
+
+ public int VertexCount => path.Vertices.Count;
+
+ protected readonly Func PositionToDistance;
+
+ protected IReadOnlyList VertexStates => vertexStates;
+
+ private readonly JuiceStreamPath path = new JuiceStreamPath();
+
+ // Invariant: `path.Vertices.Count == vertexStates.Count`
+ private readonly List vertexStates = new List
+ {
+ new VertexState { IsFixed = true }
+ };
+
+ private readonly List previousVertexStates = new List();
+
+ [Resolved(CanBeNull = true)]
+ [CanBeNull]
+ private IBeatSnapProvider beatSnapProvider { get; set; }
+
+ protected EditablePath(Func positionToDistance)
+ {
+ PositionToDistance = positionToDistance;
+
+ Anchor = Anchor.BottomLeft;
+ }
+
+ public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject)
+ {
+ while (path.Vertices.Count < InternalChildren.Count)
+ RemoveInternal(InternalChildren[^1]);
+
+ while (InternalChildren.Count < path.Vertices.Count)
+ AddInternal(new VertexPiece());
+
+ double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity);
+
+ for (int i = 0; i < VertexCount; i++)
+ {
+ var piece = (VertexPiece)InternalChildren[i];
+ var vertex = path.Vertices[i];
+ piece.Position = new Vector2(vertex.X, (float)(vertex.Distance * distanceToYFactor));
+ piece.UpdateFrom(vertexStates[i]);
+ }
+ }
+
+ public void InitializeFromHitObject(JuiceStream hitObject)
+ {
+ var sliderPath = hitObject.Path;
+ path.ConvertFromSliderPath(sliderPath);
+
+ // If the original slider path has non-linear type segments, resample the vertices at nested hit object times to reduce the number of vertices.
+ if (sliderPath.ControlPoints.Any(p => p.Type.Value != null && p.Type.Value != PathType.Linear))
+ {
+ path.ResampleVertices(hitObject.NestedHitObjects
+ .Skip(1).TakeWhile(h => !(h is Fruit)) // Only droplets in the first span are used.
+ .Select(h => (h.StartTime - hitObject.StartTime) * hitObject.Velocity));
+ }
+
+ vertexStates.Clear();
+ vertexStates.AddRange(path.Vertices.Select((_, i) => new VertexState
+ {
+ IsFixed = i == 0
+ }));
+ }
+
+ public void UpdateHitObjectFromPath(JuiceStream hitObject)
+ {
+ path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY);
+
+ if (beatSnapProvider == null) return;
+
+ double endTime = hitObject.StartTime + path.Distance / hitObject.Velocity;
+ double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime);
+ hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity;
+ }
+
+ public Vector2 ToRelativePosition(Vector2 screenSpacePosition)
+ {
+ return ToLocalSpace(screenSpacePosition) - new Vector2(0, DrawHeight);
+ }
+
+ protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
+
+ protected int AddVertex(double distance, float x)
+ {
+ int index = path.InsertVertex(distance);
+ path.SetVertexPosition(index, x);
+ vertexStates.Insert(index, new VertexState());
+
+ correctFixedVertexPositions();
+
+ Debug.Assert(vertexStates.Count == VertexCount);
+ return index;
+ }
+
+ protected bool RemoveVertex(int index)
+ {
+ if (index < 0 || index >= path.Vertices.Count)
+ return false;
+
+ if (vertexStates[index].IsFixed)
+ return false;
+
+ path.RemoveVertices((_, i) => i == index);
+
+ vertexStates.RemoveAt(index);
+ if (vertexStates.Count == 0)
+ vertexStates.Add(new VertexState());
+
+ Debug.Assert(vertexStates.Count == VertexCount);
+ return true;
+ }
+
+ protected void MoveSelectedVertices(double distanceDelta, float xDelta)
+ {
+ // Because the vertex list may be reordered due to distance change, the state list must be reordered as well.
+ previousVertexStates.Clear();
+ previousVertexStates.AddRange(vertexStates);
+
+ // We will recreate the path from scratch. Note that `Clear` leaves the first vertex.
+ int vertexCount = VertexCount;
+ path.Clear();
+ vertexStates.RemoveRange(1, vertexCount - 1);
+
+ for (int i = 1; i < vertexCount; i++)
+ {
+ var state = previousVertexStates[i];
+ double distance = state.VertexBeforeChange.Distance;
+ if (state.IsSelected)
+ distance += distanceDelta;
+
+ int newIndex = path.InsertVertex(Math.Max(0, distance));
+ vertexStates.Insert(newIndex, state);
+ }
+
+ // First, restore positions of the non-selected vertices.
+ for (int i = 0; i < vertexCount; i++)
+ {
+ if (!vertexStates[i].IsSelected && !vertexStates[i].IsFixed)
+ path.SetVertexPosition(i, vertexStates[i].VertexBeforeChange.X);
+ }
+
+ // Then, move the selected vertices.
+ for (int i = 0; i < vertexCount; i++)
+ {
+ if (vertexStates[i].IsSelected && !vertexStates[i].IsFixed)
+ path.SetVertexPosition(i, vertexStates[i].VertexBeforeChange.X + xDelta);
+ }
+
+ // Finally, correct the position of fixed vertices.
+ correctFixedVertexPositions();
+ }
+
+ private void correctFixedVertexPositions()
+ {
+ for (int i = 0; i < VertexCount; i++)
+ {
+ if (vertexStates[i].IsFixed)
+ path.SetVertexPosition(i, vertexStates[i].VertexBeforeChange.X);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs
new file mode 100644
index 0000000000..8c7314d0b6
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs
@@ -0,0 +1,130 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using JetBrains.Annotations;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Input.Events;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Screens.Edit;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
+{
+ public class SelectionEditablePath : EditablePath, IHasContextMenu
+ {
+ public MenuItem[] ContextMenuItems => getContextMenuItems().ToArray();
+
+ // To handle when the editor is scrolled while dragging.
+ private Vector2 dragStartPosition;
+
+ [Resolved(CanBeNull = true)]
+ [CanBeNull]
+ private IEditorChangeHandler changeHandler { get; set; }
+
+ public SelectionEditablePath(Func positionToDistance)
+ : base(positionToDistance)
+ {
+ }
+
+ public void AddVertex(Vector2 relativePosition)
+ {
+ double distance = Math.Max(0, PositionToDistance(relativePosition.Y));
+ int index = AddVertex(distance, relativePosition.X);
+ selectOnly(index);
+ }
+
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => InternalChildren.Any(d => d.ReceivePositionalInputAt(screenSpacePos));
+
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ int index = getMouseTargetVertex(e.ScreenSpaceMouseDownPosition);
+ if (index == -1 || VertexStates[index].IsFixed)
+ return false;
+
+ if (e.Button == MouseButton.Left && e.ShiftPressed)
+ {
+ RemoveVertex(index);
+ return true;
+ }
+
+ if (e.ControlPressed)
+ VertexStates[index].IsSelected = !VertexStates[index].IsSelected;
+ else if (!VertexStates[index].IsSelected)
+ selectOnly(index);
+
+ // Don't inhibit right click, to show the context menu
+ return e.Button != MouseButton.Right;
+ }
+
+ protected override bool OnDragStart(DragStartEvent e)
+ {
+ int index = getMouseTargetVertex(e.ScreenSpaceMouseDownPosition);
+ if (index == -1 || VertexStates[index].IsFixed)
+ return false;
+
+ if (e.Button != MouseButton.Left)
+ return false;
+
+ dragStartPosition = ToRelativePosition(e.ScreenSpaceMouseDownPosition);
+
+ for (int i = 0; i < VertexCount; i++)
+ VertexStates[i].VertexBeforeChange = Vertices[i];
+
+ changeHandler?.BeginChange();
+ return true;
+ }
+
+ protected override void OnDrag(DragEvent e)
+ {
+ Vector2 mousePosition = ToRelativePosition(e.ScreenSpaceMousePosition);
+ double distanceDelta = PositionToDistance(mousePosition.Y) - PositionToDistance(dragStartPosition.Y);
+ float xDelta = mousePosition.X - dragStartPosition.X;
+ MoveSelectedVertices(distanceDelta, xDelta);
+ }
+
+ protected override void OnDragEnd(DragEndEvent e)
+ {
+ changeHandler?.EndChange();
+ }
+
+ private int getMouseTargetVertex(Vector2 screenSpacePosition)
+ {
+ for (int i = InternalChildren.Count - 1; i >= 0; i--)
+ {
+ if (i < VertexCount && InternalChildren[i].ReceivePositionalInputAt(screenSpacePosition))
+ return i;
+ }
+
+ return -1;
+ }
+
+ private IEnumerable
-
-
+
+
@@ -93,7 +93,7 @@
-
+
diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings
index 7284ca1a9a..139ee02b96 100644
--- a/osu.sln.DotSettings
+++ b/osu.sln.DotSettings
@@ -19,8 +19,8 @@
HINT
DO_NOT_SHOW
WARNING
- WARNING
- WARNING
+ HINT
+ HINT
WARNING
WARNING
WARNING