diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 729f2f266d..1d5c565d60 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -31,7 +31,7 @@ jobs:
uses: actions/cache@v3
with:
path: ${{ github.workspace }}/inspectcode
- key: inspectcode-${{ hashFiles('.config/dotnet-tools.json', '.github/workflows/ci.yml', 'osu.sln*', '.editorconfig', '.globalconfig') }}
+ key: inspectcode-${{ hashFiles('.config/dotnet-tools.json', '.github/workflows/ci.yml', 'osu.sln*', '.editorconfig', '.globalconfig', 'CodeAnalysis/*') }}
- name: Dotnet code style
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf -p:EnforceCodeStyleInBuild=true
diff --git a/.github/workflows/sentry-release.yml b/.github/workflows/sentry-release.yml
new file mode 100644
index 0000000000..8ca9f38234
--- /dev/null
+++ b/.github/workflows/sentry-release.yml
@@ -0,0 +1,26 @@
+name: Add Release to Sentry
+
+on:
+ push:
+ tags:
+ - '*'
+
+jobs:
+ sentry_release:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ with:
+ fetch-depth: 0
+
+ - name: Create Sentry release
+ uses: getsentry/action-release@v1
+ env:
+ SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
+ SENTRY_ORG: ppy
+ SENTRY_PROJECT: osu
+ SENTRY_URL: https://sentry.ppy.sh/
+ with:
+ environment: production
+ version: ${{ github.ref }}
diff --git a/.gitignore b/.gitignore
index 5b19270ab9..0c7a18b437 100644
--- a/.gitignore
+++ b/.gitignore
@@ -340,3 +340,5 @@ inspectcode
# Fody (pulled in by Realm) - schema file
FodyWeavers.xsd
**/FodyWeavers.xml
+
+.idea/.idea.osu.Desktop/.idea/misc.xml
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/misc.xml b/.idea/.idea.osu.Desktop/.idea/misc.xml
deleted file mode 100644
index 4e1d56f4dd..0000000000
--- a/.idea/.idea.osu.Desktop/.idea/misc.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt
index e96ad48325..b72df0a306 100644
--- a/CodeAnalysis/BannedSymbols.txt
+++ b/CodeAnalysis/BannedSymbols.txt
@@ -11,6 +11,7 @@ T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal exten
T:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods.
M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast() instead.
M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection,NotificationCallbackDelegate) instead.
+M:System.Guid.#ctor;Probably meaning to use Guid.NewGuid() instead. If actually wanting empty, use Guid.Empty.
M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IQueryable,NotificationCallbackDelegate) instead.
M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IList,NotificationCallbackDelegate) instead.
M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks.
diff --git a/osu.Android.props b/osu.Android.props
index 27fcdd4f6e..7910100b1f 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -56,6 +56,6 @@
-
+
diff --git a/osu.Desktop/osu.nuspec b/osu.Desktop/osu.nuspec
index dc1ec17e2c..db58c325bd 100644
--- a/osu.Desktop/osu.nuspec
+++ b/osu.Desktop/osu.nuspec
@@ -18,5 +18,6 @@
+
diff --git a/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist b/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist
index 33ddac6dfb..16a2b99997 100644
--- a/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist
+++ b/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist
@@ -37,6 +37,8 @@
XSAppIconAssets
Assets.xcassets/AppIcon.appiconset
+ UIApplicationSupportsIndirectInputEvents
+
CADisableMinimumFrameDurationOnPhone
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs
index 981efc9a13..b1adc4901c 100644
--- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public class TestSceneJuiceStreamPlacementBlueprint : CatchPlacementBlueprintTestScene
{
- private const double velocity = 0.5;
+ private const double velocity_factor = 0.5;
private JuiceStream lastObject => LastObject?.HitObject as JuiceStream;
@@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
{
var playable = base.GetPlayableBeatmap();
playable.Difficulty.SliderTickRate = 5;
- playable.Difficulty.SliderMultiplier = velocity * 10;
+ playable.Difficulty.SliderMultiplier = velocity_factor * 10;
return playable;
}
@@ -43,6 +43,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddAssert("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1]));
AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0]));
AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1]));
+ AddAssert("default slider velocity", () => lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
}
[Test]
@@ -66,28 +67,21 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
}
[Test]
- public void TestVelocityLimit()
+ public void TestSliderVelocityChange()
{
double[] times = { 100, 300 };
float[] positions = { 200, 500 };
addPlacementSteps(times, positions);
- addPathCheckStep(times, new float[] { 200, 300 });
- }
+ addPathCheckStep(times, positions);
- [Test]
- public void TestPreviousVerticesAreFixed()
- {
- double[] times = { 100, 300, 500, 700 };
- float[] positions = { 200, 400, 100, 500 };
- addPlacementSteps(times, positions);
- addPathCheckStep(times, new float[] { 200, 300, 200, 300 });
+ AddAssert("slider velocity changed", () => !lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
}
[Test]
public void TestClampedPositionIsRestored()
{
double[] times = { 100, 300, 500 };
- float[] positions = { 200, 200, 0, 250 };
+ float[] positions = { 200, 200, -3000, 250 };
addMoveAndClickSteps(times[0], positions[0]);
addMoveAndClickSteps(times[1], positions[1]);
@@ -97,15 +91,6 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
addPathCheckStep(times, new float[] { 200, 200, 250 });
}
- [Test]
- public void TestFirstVertexIsFixed()
- {
- double[] times = { 100, 200 };
- float[] positions = { 100, 300 };
- addPlacementSteps(times, positions);
- addPathCheckStep(times, new float[] { 100, 150 });
- }
-
[Test]
public void TestOutOfOrder()
{
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs
index fb77fb1efd..22a839d847 100644
--- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs
@@ -101,31 +101,16 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
}
[Test]
- public void TestClampedPositionIsRestored()
+ public void TestSliderVelocityChange()
{
- const double velocity = 0.25;
- double[] times = { 100, 500, 700 };
- float[] positions = { 100, 100, 100 };
- addBlueprintStep(times, positions, velocity);
+ double[] times = { 100, 300 };
+ float[] positions = { 200, 300 };
+ addBlueprintStep(times, positions);
+ AddAssert("default slider velocity", () => hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
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);
+ AddMouseMoveStep(times[1], 400);
+ AddAssert("slider velocity changed", () => !hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
}
[Test]
@@ -174,7 +159,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
addAddVertexSteps(500, 150);
addVertexCheckStep(3, 1, 500, 150);
- addAddVertexSteps(90, 220);
+ addAddVertexSteps(90, 200);
addVertexCheckStep(4, 1, times[0], positions[0]);
addAddVertexSteps(750, 180);
@@ -234,10 +219,10 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
{
var path = new JuiceStreamPath();
for (int i = 1; i < times.Length; i++)
- path.Add((times[i] - times[0]) * velocity, positions[i] - positions[0]);
+ path.Add(times[i] - times[0], positions[i] - positions[0]);
var sliderPath = new SliderPath();
- path.ConvertToSliderPath(sliderPath, 0);
+ path.ConvertToSliderPath(sliderPath, 0, velocity);
addBlueprintStep(times[0], positions[0], sliderPath, velocity);
}
@@ -245,11 +230,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
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;
+ double expectedTime = time - hitObject.StartTime;
float expectedX = x - hitObject.OriginalX;
var vertices = getVertices();
return vertices.Count == count &&
- Precision.AlmostEquals(vertices[index].Distance, expectedDistance, 1e-3) &&
+ Precision.AlmostEquals(vertices[index].Time, expectedTime, 1e-3) &&
Precision.AlmostEquals(vertices[index].X, expectedX);
});
diff --git a/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs b/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs
index 8fa96fb8c9..5248d5a96a 100644
--- a/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs
+++ b/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs
@@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
-using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@@ -37,14 +36,14 @@ namespace osu.Game.Rulesets.Catch.Tests
{
case 0:
{
- double distance = rng.NextDouble() * scale * 2 - scale;
+ double time = rng.NextDouble() * scale * 2 - scale;
if (integralValues)
- distance = Math.Round(distance);
+ time = Math.Round(time);
- float oldX = path.PositionAtDistance(distance);
- int index = path.InsertVertex(distance);
+ float oldX = path.PositionAtTime(time);
+ int index = path.InsertVertex(time);
Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount + 1));
- Assert.That(path.Vertices[index].Distance, Is.EqualTo(distance));
+ Assert.That(path.Vertices[index].Time, Is.EqualTo(time));
Assert.That(path.Vertices[index].X, Is.EqualTo(oldX));
break;
}
@@ -52,20 +51,20 @@ namespace osu.Game.Rulesets.Catch.Tests
case 1:
{
int index = rng.Next(path.Vertices.Count);
- double distance = path.Vertices[index].Distance;
+ double time = path.Vertices[index].Time;
float newX = (float)(rng.NextDouble() * scale * 2 - scale);
if (integralValues)
newX = MathF.Round(newX);
path.SetVertexPosition(index, newX);
Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount));
- Assert.That(path.Vertices[index].Distance, Is.EqualTo(distance));
+ Assert.That(path.Vertices[index].Time, Is.EqualTo(time));
Assert.That(path.Vertices[index].X, Is.EqualTo(newX));
break;
}
}
- assertInvariants(path.Vertices, checkSlope);
+ assertInvariants(path.Vertices);
}
}
@@ -76,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Tests
path.Add(10, 5);
path.Add(20, -5);
- int removeCount = path.RemoveVertices((v, i) => v.Distance == 10 && i == 1);
+ int removeCount = path.RemoveVertices((v, i) => v.Time == 10 && i == 1);
Assert.That(removeCount, Is.EqualTo(1));
Assert.That(path.Vertices, Is.EqualTo(new[]
{
@@ -131,8 +130,9 @@ namespace osu.Game.Rulesets.Catch.Tests
}));
}
- [Test]
- public void TestRandomConvertFromSliderPath()
+ [TestCase(10)]
+ [TestCase(0.1)]
+ public void TestRandomConvertFromSliderPath(double velocity)
{
var rng = new Random(1);
var path = new JuiceStreamPath();
@@ -162,28 +162,28 @@ namespace osu.Game.Rulesets.Catch.Tests
else
sliderPath.ExpectedDistance.Value = null;
- path.ConvertFromSliderPath(sliderPath);
- Assert.That(path.Vertices[0].Distance, Is.EqualTo(0));
- Assert.That(path.Distance, Is.EqualTo(sliderPath.Distance).Within(1e-3));
- assertInvariants(path.Vertices, true);
+ path.ConvertFromSliderPath(sliderPath, velocity);
+ Assert.That(path.Vertices[0].Time, Is.EqualTo(0));
+ Assert.That(path.Duration * velocity, Is.EqualTo(sliderPath.Distance).Within(1e-3));
+ assertInvariants(path.Vertices);
- double[] sampleDistances = Enumerable.Range(0, 10)
- .Select(_ => rng.NextDouble() * sliderPath.Distance)
- .ToArray();
+ double[] sampleTimes = Enumerable.Range(0, 10)
+ .Select(_ => rng.NextDouble() * sliderPath.Distance / velocity)
+ .ToArray();
- foreach (double distance in sampleDistances)
+ foreach (double time in sampleTimes)
{
- float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X;
- Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3));
+ float expected = sliderPath.PositionAt(time * velocity / sliderPath.Distance).X;
+ Assert.That(path.PositionAtTime(time), Is.EqualTo(expected).Within(1e-3));
}
- path.ResampleVertices(sampleDistances);
- assertInvariants(path.Vertices, true);
+ path.ResampleVertices(sampleTimes);
+ assertInvariants(path.Vertices);
- foreach (double distance in sampleDistances)
+ foreach (double time in sampleTimes)
{
- float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X;
- Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3));
+ float expected = sliderPath.PositionAt(time * velocity / sliderPath.Distance).X;
+ Assert.That(path.PositionAtTime(time), Is.EqualTo(expected).Within(1e-3));
}
}
}
@@ -201,17 +201,17 @@ namespace osu.Game.Rulesets.Catch.Tests
do
{
- double distance = rng.NextDouble() * 1e3;
+ double time = rng.NextDouble() * 1e3;
float x = (float)(rng.NextDouble() * 1e3);
- path.Add(distance, x);
+ path.Add(time, x);
} while (rng.Next(5) != 0);
float sliderStartY = (float)(rng.NextDouble() * JuiceStreamPath.OSU_PLAYFIELD_HEIGHT);
- path.ConvertToSliderPath(sliderPath, sliderStartY);
- Assert.That(sliderPath.Distance, Is.EqualTo(path.Distance).Within(1e-3));
- Assert.That(sliderPath.ControlPoints[0].Position.X, Is.EqualTo(path.Vertices[0].X));
- assertInvariants(path.Vertices, true);
+ double requiredVelocity = path.ComputeRequiredVelocity();
+ double velocity = Math.Clamp(requiredVelocity, 1, 100);
+
+ path.ConvertToSliderPath(sliderPath, sliderStartY, velocity);
foreach (var point in sliderPath.ControlPoints)
{
@@ -219,11 +219,18 @@ namespace osu.Game.Rulesets.Catch.Tests
Assert.That(sliderStartY + point.Position.Y, Is.InRange(0, JuiceStreamPath.OSU_PLAYFIELD_HEIGHT));
}
+ Assert.That(sliderPath.ControlPoints[0].Position.X, Is.EqualTo(path.Vertices[0].X));
+
+ // The path is preserved only if required velocity is used.
+ if (velocity < requiredVelocity) continue;
+
+ Assert.That(sliderPath.Distance / velocity, Is.EqualTo(path.Duration).Within(1e-3));
+
for (int i = 0; i < 10; i++)
{
- double distance = rng.NextDouble() * path.Distance;
- float expected = path.PositionAtDistance(distance);
- Assert.That(sliderPath.PositionAt(distance / sliderPath.Distance).X, Is.EqualTo(expected).Within(1e-3));
+ double time = rng.NextDouble() * path.Duration;
+ float expected = path.PositionAtTime(time);
+ Assert.That(sliderPath.PositionAt(time * velocity / sliderPath.Distance).X, Is.EqualTo(expected).Within(3e-3));
}
}
}
@@ -244,7 +251,7 @@ namespace osu.Game.Rulesets.Catch.Tests
path.Add(20, 0);
checkNewId();
- path.RemoveVertices((v, _) => v.Distance == 20);
+ path.RemoveVertices((v, _) => v.Time == 20);
checkNewId();
path.ResampleVertices(new double[] { 5, 10, 15 });
@@ -253,7 +260,7 @@ namespace osu.Game.Rulesets.Catch.Tests
path.Clear();
checkNewId();
- path.ConvertFromSliderPath(new SliderPath());
+ path.ConvertFromSliderPath(new SliderPath(), 1);
checkNewId();
void checkNewId()
@@ -263,25 +270,19 @@ namespace osu.Game.Rulesets.Catch.Tests
}
}
- private void assertInvariants(IReadOnlyList vertices, bool checkSlope)
+ private void assertInvariants(IReadOnlyList vertices)
{
Assert.That(vertices, Is.Not.Empty);
for (int i = 0; i < vertices.Count; i++)
{
- Assert.That(double.IsFinite(vertices[i].Distance));
+ Assert.That(double.IsFinite(vertices[i].Time));
Assert.That(float.IsFinite(vertices[i].X));
}
for (int i = 1; i < vertices.Count; i++)
{
- Assert.That(vertices[i].Distance, Is.GreaterThanOrEqualTo(vertices[i - 1].Distance));
-
- if (!checkSlope) continue;
-
- float xDiff = Math.Abs(vertices[i].X - vertices[i - 1].X);
- double distanceDiff = vertices[i].Distance - vertices[i - 1].Distance;
- Assert.That(xDiff, Is.LessThanOrEqualTo(distanceDiff).Within(Precision.FLOAT_EPSILON));
+ Assert.That(vertices[i].Time, Is.GreaterThanOrEqualTo(vertices[i - 1].Time));
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs
index 1a43a10c81..e038562b4b 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
public int VertexCount => path.Vertices.Count;
- protected readonly Func PositionToDistance;
+ protected readonly Func PositionToTime;
protected IReadOnlyList VertexStates => vertexStates;
@@ -44,9 +44,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
[CanBeNull]
private IBeatSnapProvider beatSnapProvider { get; set; }
- protected EditablePath(Func positionToDistance)
+ protected EditablePath(Func positionToTime)
{
- PositionToDistance = positionToDistance;
+ PositionToTime = positionToTime;
Anchor = Anchor.BottomLeft;
}
@@ -59,13 +59,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
while (InternalChildren.Count < path.Vertices.Count)
AddInternal(new VertexPiece());
- double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity);
+ double timeToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1);
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.Position = new Vector2(vertex.X, (float)(vertex.Time * timeToYFactor));
piece.UpdateFrom(vertexStates[i]);
}
}
@@ -73,14 +73,14 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
public void InitializeFromHitObject(JuiceStream hitObject)
{
var sliderPath = hitObject.Path;
- path.ConvertFromSliderPath(sliderPath);
+ path.ConvertFromSliderPath(sliderPath, hitObject.Velocity);
// 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 != null && p.Type != 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));
+ .Select(h => h.StartTime - hitObject.StartTime));
}
vertexStates.Clear();
@@ -92,11 +92,26 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
public void UpdateHitObjectFromPath(JuiceStream hitObject)
{
- path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY);
+ // The SV setting may need to be changed for the current path.
+ var svBindable = hitObject.DifficultyControlPoint.SliderVelocityBindable;
+ double svToVelocityFactor = hitObject.Velocity / svBindable.Value;
+ double requiredVelocity = path.ComputeRequiredVelocity();
+
+ // The value is pre-rounded here because setting it to the bindable will rounded to the nearest value
+ // but it should be always rounded up to satisfy the required minimum velocity condition.
+ //
+ // This is rounded to integers instead of using the precision of the bindable
+ // because it results in a smaller number of non-redundant control points.
+ //
+ // The value is clamped here by the bindable min and max values.
+ // In case the required velocity is too large, the path is not preserved.
+ svBindable.Value = Math.Ceiling(requiredVelocity / svToVelocityFactor);
+
+ path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY, hitObject.Velocity);
if (beatSnapProvider == null) return;
- double endTime = hitObject.StartTime + path.Distance / hitObject.Velocity;
+ double endTime = hitObject.StartTime + path.Duration;
double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime);
hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity;
}
@@ -108,9 +123,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
- protected int AddVertex(double distance, float x)
+ protected int AddVertex(double time, float x)
{
- int index = path.InsertVertex(distance);
+ int index = path.InsertVertex(time);
path.SetVertexPosition(index, x);
vertexStates.Insert(index, new VertexState());
@@ -138,9 +153,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
return true;
}
- protected void MoveSelectedVertices(double distanceDelta, float xDelta)
+ protected void MoveSelectedVertices(double timeDelta, float xDelta)
{
- // Because the vertex list may be reordered due to distance change, the state list must be reordered as well.
+ // Because the vertex list may be reordered due to time change, the state list must be reordered as well.
previousVertexStates.Clear();
previousVertexStates.AddRange(vertexStates);
@@ -152,11 +167,11 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
for (int i = 1; i < vertexCount; i++)
{
var state = previousVertexStates[i];
- double distance = state.VertexBeforeChange.Distance;
+ double time = state.VertexBeforeChange.Time;
if (state.IsSelected)
- distance += distanceDelta;
+ time += timeDelta;
- int newIndex = path.InsertVertex(Math.Max(0, distance));
+ int newIndex = path.InsertVertex(Math.Max(0, time));
vertexStates.Insert(newIndex, state);
}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/PlacementEditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/PlacementEditablePath.cs
index 158872fbab..511aec5e5d 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/PlacementEditablePath.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/PlacementEditablePath.cs
@@ -15,15 +15,15 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
///
private JuiceStreamPathVertex lastVertex;
- public PlacementEditablePath(Func positionToDistance)
- : base(positionToDistance)
+ public PlacementEditablePath(Func positionToTime)
+ : base(positionToTime)
{
}
public void AddNewVertex()
{
var endVertex = Vertices[^1];
- int index = AddVertex(endVertex.Distance, endVertex.X);
+ int index = AddVertex(endVertex.Time, endVertex.X);
for (int i = 0; i < VertexCount; i++)
{
@@ -41,9 +41,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
public void MoveLastVertex(Vector2 screenSpacePosition)
{
Vector2 position = ToRelativePosition(screenSpacePosition);
- double distanceDelta = PositionToDistance(position.Y) - lastVertex.Distance;
+ double timeDelta = PositionToTime(position.Y) - lastVertex.Time;
float xDelta = position.X - lastVertex.X;
- MoveSelectedVertices(distanceDelta, xDelta);
+ MoveSelectedVertices(timeDelta, xDelta);
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/ScrollingPath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/ScrollingPath.cs
index 109bf61ea5..cfaca2f9a4 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/ScrollingPath.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/ScrollingPath.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{
private readonly Path drawablePath;
- private readonly List<(double Distance, float X)> vertices = new List<(double, float)>();
+ private readonly List<(double Time, float X)> vertices = new List<(double, float)>();
public ScrollingPath()
{
@@ -35,16 +35,16 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
public void UpdatePathFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject)
{
- double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity);
+ double timeToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1);
- computeDistanceXs(hitObject);
+ computeTimeXs(hitObject);
drawablePath.Vertices = vertices
- .Select(v => new Vector2(v.X, (float)(v.Distance * distanceToYFactor)))
+ .Select(v => new Vector2(v.X, (float)(v.Time * timeToYFactor)))
.ToArray();
drawablePath.OriginPosition = drawablePath.PositionInBoundingBox(Vector2.Zero);
}
- private void computeDistanceXs(JuiceStream hitObject)
+ private void computeTimeXs(JuiceStream hitObject)
{
vertices.Clear();
@@ -54,17 +54,17 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
if (sliderVertices.Count == 0)
return;
- double distance = 0;
+ double time = 0;
Vector2 lastPosition = Vector2.Zero;
for (int repeat = 0; repeat < hitObject.RepeatCount + 1; repeat++)
{
foreach (var position in sliderVertices)
{
- distance += Vector2.Distance(lastPosition, position);
+ time += Vector2.Distance(lastPosition, position) / hitObject.Velocity;
lastPosition = position;
- vertices.Add((distance, position.X));
+ vertices.Add((time, position.X));
}
sliderVertices.Reverse();
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs
index 8c7314d0b6..b4c353313c 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs
@@ -27,15 +27,15 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
[CanBeNull]
private IEditorChangeHandler changeHandler { get; set; }
- public SelectionEditablePath(Func positionToDistance)
- : base(positionToDistance)
+ public SelectionEditablePath(Func positionToTime)
+ : base(positionToTime)
{
}
public void AddVertex(Vector2 relativePosition)
{
- double distance = Math.Max(0, PositionToDistance(relativePosition.Y));
- int index = AddVertex(distance, relativePosition.X);
+ double time = Math.Max(0, PositionToTime(relativePosition.Y));
+ int index = AddVertex(time, relativePosition.X);
selectOnly(index);
}
@@ -83,9 +83,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
protected override void OnDrag(DragEvent e)
{
Vector2 mousePosition = ToRelativePosition(e.ScreenSpaceMousePosition);
- double distanceDelta = PositionToDistance(mousePosition.Y) - PositionToDistance(dragStartPosition.Y);
+ double timeDelta = PositionToTime(mousePosition.Y) - PositionToTime(dragStartPosition.Y);
float xDelta = mousePosition.X - dragStartPosition.X;
- MoveSelectedVertices(distanceDelta, xDelta);
+ MoveSelectedVertices(timeDelta, xDelta);
}
protected override void OnDragEnd(DragEndEvent e)
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs
index e9c8e2bb2c..b5dcb62543 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{
scrollingPath = new ScrollingPath(),
nestedOutlineContainer = new NestedOutlineContainer(),
- editablePath = new PlacementEditablePath(positionToDistance)
+ editablePath = new PlacementEditablePath(positionToTime)
};
}
@@ -121,10 +121,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
lastEditablePathId = editablePath.PathId;
}
- private double positionToDistance(float relativeYPosition)
+ private double positionToTime(float relativeYPosition)
{
double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime);
- return (time - HitObject.StartTime) * HitObject.Velocity;
+ return time - HitObject.StartTime;
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs
index 890d059d19..12054a1d16 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs
@@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{
scrollingPath = new ScrollingPath(),
nestedOutlineContainer = new NestedOutlineContainer(),
- editablePath = new SelectionEditablePath(positionToDistance)
+ editablePath = new SelectionEditablePath(positionToTime)
};
}
@@ -145,10 +145,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
return new RectangleF(left, top, right - left, bottom - top).Inflate(objectRadius);
}
- private double positionToDistance(float relativeYPosition)
+ private double positionToTime(float relativeYPosition)
{
double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime);
- return (time - HitObject.StartTime) * HitObject.Velocity;
+ return time - HitObject.StartTime;
}
private void initializeJuiceStreamPath()
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
index 630a2cf645..6f59b3e543 100644
--- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
@@ -6,6 +6,7 @@ using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
@@ -89,15 +90,19 @@ namespace osu.Game.Rulesets.Catch.Edit
new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler })
});
- public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
+ public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
{
- var result = base.FindSnappedPositionAndTime(screenSpacePosition);
+ var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
+
result.ScreenSpacePosition.X = screenSpacePosition.X;
- if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
- Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius)
+ if (snapType.HasFlagFast(SnapType.Grids))
{
- result = snapResult;
+ if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
+ Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius)
+ {
+ result = snapResult;
+ }
}
return result;
diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
index 282afb6343..d34452cdbb 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
@@ -27,10 +27,16 @@ namespace osu.Game.Rulesets.Catch.Objects
public int RepeatCount { get; set; }
[JsonIgnore]
- public double Velocity { get; private set; }
+ private double velocityFactor;
[JsonIgnore]
- public double TickDistance { get; private set; }
+ private double tickDistanceFactor;
+
+ [JsonIgnore]
+ public double Velocity => velocityFactor * DifficultyControlPoint.SliderVelocity;
+
+ [JsonIgnore]
+ public double TickDistance => tickDistanceFactor * DifficultyControlPoint.SliderVelocity;
///
/// The length of one span of this .
@@ -43,10 +49,8 @@ namespace osu.Game.Rulesets.Catch.Objects
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
- double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
-
- Velocity = scoringDistance / timingPoint.BeatLength;
- TickDistance = scoringDistance / difficulty.SliderTickRate;
+ velocityFactor = base_scoring_distance * difficulty.SliderMultiplier / timingPoint.BeatLength;
+ tickDistanceFactor = base_scoring_distance * difficulty.SliderMultiplier / difficulty.SliderTickRate;
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs
index 7207833fe6..61f4c580ae 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs
@@ -20,11 +20,6 @@ namespace osu.Game.Rulesets.Catch.Objects
/// However, the representation is difficult to work with.
/// This represents the path in a more convenient way, a polyline connecting list of s.
///
- ///
- /// The path can be regarded as a function from the closed interval [Vertices[0].Distance, Vertices[^1].Distance] to the x position, given by .
- /// To ensure the path is convertible to a , the slope of the function must not be more than 1 everywhere,
- /// and this slope condition is always maintained as an invariant.
- ///
///
public class JuiceStreamPath
{
@@ -46,9 +41,9 @@ namespace osu.Game.Rulesets.Catch.Objects
public int InvalidationID { get; private set; } = 1;
///
- /// The difference between first vertex's and last vertex's .
+ /// The difference between first vertex's and last vertex's .
///
- public double Distance => vertices[^1].Distance - vertices[0].Distance;
+ public double Duration => vertices[^1].Time - vertices[0].Time;
///
/// This list should always be non-empty.
@@ -59,15 +54,15 @@ namespace osu.Game.Rulesets.Catch.Objects
};
///
- /// Compute the x-position of the path at the given .
+ /// Compute the x-position of the path at the given .
///
///
- /// When the given distance is outside of the path, the x position at the corresponding endpoint is returned,
+ /// When the given time is outside of the path, the x position at the corresponding endpoint is returned,
///
- public float PositionAtDistance(double distance)
+ public float PositionAtTime(double time)
{
- int index = vertexIndexAtDistance(distance);
- return positionAtDistance(distance, index);
+ int index = vertexIndexAtTime(time);
+ return positionAtTime(time, index);
}
///
@@ -81,19 +76,19 @@ namespace osu.Game.Rulesets.Catch.Objects
}
///
- /// Insert a vertex at given .
- /// The is used as the position of the new vertex.
+ /// Insert a vertex at given .
+ /// The is used as the position of the new vertex.
/// Thus, the set of points of the path is not changed (up to floating-point precision).
///
/// The index of the new vertex.
- public int InsertVertex(double distance)
+ public int InsertVertex(double time)
{
- if (!double.IsFinite(distance))
- throw new ArgumentOutOfRangeException(nameof(distance));
+ if (!double.IsFinite(time))
+ throw new ArgumentOutOfRangeException(nameof(time));
- int index = vertexIndexAtDistance(distance);
- float x = positionAtDistance(distance, index);
- vertices.Insert(index, new JuiceStreamPathVertex(distance, x));
+ int index = vertexIndexAtTime(time);
+ float x = positionAtTime(time, index);
+ vertices.Insert(index, new JuiceStreamPathVertex(time, x));
invalidate();
return index;
@@ -101,7 +96,6 @@ namespace osu.Game.Rulesets.Catch.Objects
///
/// Move the vertex of given to the given position .
- /// When the distances between vertices are too small for the new vertex positions, the adjacent vertices are moved towards .
///
public void SetVertexPosition(int index, float newX)
{
@@ -111,32 +105,17 @@ namespace osu.Game.Rulesets.Catch.Objects
if (!float.IsFinite(newX))
throw new ArgumentOutOfRangeException(nameof(newX));
- var newVertex = new JuiceStreamPathVertex(vertices[index].Distance, newX);
-
- for (int i = index - 1; i >= 0 && !canConnect(vertices[i], newVertex); i--)
- {
- float clampedX = clampToConnectablePosition(newVertex, vertices[i]);
- vertices[i] = new JuiceStreamPathVertex(vertices[i].Distance, clampedX);
- }
-
- for (int i = index + 1; i < vertices.Count; i++)
- {
- float clampedX = clampToConnectablePosition(newVertex, vertices[i]);
- vertices[i] = new JuiceStreamPathVertex(vertices[i].Distance, clampedX);
- }
-
- vertices[index] = newVertex;
+ vertices[index] = new JuiceStreamPathVertex(vertices[index].Time, newX);
invalidate();
}
///
- /// Add a new vertex at given and position.
- /// Adjacent vertices are moved when necessary in the same way as .
+ /// Add a new vertex at given and position.
///
- public void Add(double distance, float x)
+ public void Add(double time, float x)
{
- int index = InsertVertex(distance);
+ int index = InsertVertex(time);
SetVertexPosition(index, x);
}
@@ -163,22 +142,22 @@ namespace osu.Game.Rulesets.Catch.Objects
}
///
- /// Recreate this path by using difference set of vertices at given distances.
- /// In addition to the given , the first vertex and the last vertex are always added to the new path.
- /// New vertices use the positions on the original path. Thus, s at are preserved.
+ /// Recreate this path by using difference set of vertices at given time points.
+ /// In addition to the given , the first vertex and the last vertex are always added to the new path.
+ /// New vertices use the positions on the original path. Thus, s at are preserved.
///
- public void ResampleVertices(IEnumerable sampleDistances)
+ public void ResampleVertices(IEnumerable sampleTimes)
{
var sampledVertices = new List();
- foreach (double distance in sampleDistances)
+ foreach (double time in sampleTimes)
{
- if (!double.IsFinite(distance))
- throw new ArgumentOutOfRangeException(nameof(sampleDistances));
+ if (!double.IsFinite(time))
+ throw new ArgumentOutOfRangeException(nameof(sampleTimes));
- double clampedDistance = Math.Clamp(distance, vertices[0].Distance, vertices[^1].Distance);
- float x = PositionAtDistance(clampedDistance);
- sampledVertices.Add(new JuiceStreamPathVertex(clampedDistance, x));
+ double clampedTime = Math.Clamp(time, vertices[0].Time, vertices[^1].Time);
+ float x = PositionAtTime(clampedTime);
+ sampledVertices.Add(new JuiceStreamPathVertex(clampedTime, x));
}
sampledVertices.Sort();
@@ -196,37 +175,62 @@ namespace osu.Game.Rulesets.Catch.Objects
///
/// Duplicated vertices are automatically removed.
///
- public void ConvertFromSliderPath(SliderPath sliderPath)
+ public void ConvertFromSliderPath(SliderPath sliderPath, double velocity)
{
var sliderPathVertices = new List();
sliderPath.GetPathToProgress(sliderPathVertices, 0, 1);
- double distance = 0;
+ double time = 0;
vertices.Clear();
vertices.Add(new JuiceStreamPathVertex(0, sliderPathVertices.FirstOrDefault().X));
for (int i = 1; i < sliderPathVertices.Count; i++)
{
- distance += Vector2.Distance(sliderPathVertices[i - 1], sliderPathVertices[i]);
+ time += Vector2.Distance(sliderPathVertices[i - 1], sliderPathVertices[i]) / velocity;
- if (!Precision.AlmostEquals(vertices[^1].Distance, distance))
- vertices.Add(new JuiceStreamPathVertex(distance, sliderPathVertices[i].X));
+ if (!Precision.AlmostEquals(vertices[^1].Time, time))
+ Add(time, sliderPathVertices[i].X);
}
invalidate();
}
+ ///
+ /// Computes the minimum slider velocity required to convert this path to a .
+ ///
+ public double ComputeRequiredVelocity()
+ {
+ double maximumSlope = 0;
+
+ for (int i = 1; i < vertices.Count; i++)
+ {
+ double xDifference = Math.Abs((double)vertices[i].X - vertices[i - 1].X);
+ double timeDifference = vertices[i].Time - vertices[i - 1].Time;
+
+ // A short segment won't affect the resulting path much anyways so ignore it to avoid divide-by-zero.
+ if (Precision.AlmostEquals(timeDifference, 0))
+ continue;
+
+ maximumSlope = Math.Max(maximumSlope, xDifference / timeDifference);
+ }
+
+ return maximumSlope;
+ }
+
///
/// Convert the path of this to a and write the result to .
/// The resulting slider is "folded" to make it vertically contained in the playfield `(0..)` assuming the slider start position is .
+ ///
+ /// The velocity of the converted slider is assumed to be .
+ /// To preserve the path, should be at least the value returned by .
///
- public void ConvertToSliderPath(SliderPath sliderPath, float sliderStartY)
+ public void ConvertToSliderPath(SliderPath sliderPath, float sliderStartY, double velocity)
{
const float margin = 1;
// Note: these two variables and `sliderPath` are modified by the local functions.
- double currentDistance = 0;
+ double currentTime = 0;
Vector2 lastPosition = new Vector2(vertices[0].X, 0);
sliderPath.ControlPoints.Clear();
@@ -237,10 +241,10 @@ namespace osu.Game.Rulesets.Catch.Objects
sliderPath.ControlPoints[^1].Type = PathType.Linear;
float deltaX = vertices[i].X - lastPosition.X;
- double length = vertices[i].Distance - currentDistance;
+ double length = (vertices[i].Time - currentTime) * velocity;
// Should satisfy `deltaX^2 + deltaY^2 = length^2`.
- // By invariants, the expression inside the `sqrt` is (almost) non-negative.
+ // The expression inside the `sqrt` is (almost) non-negative if the slider velocity is large enough.
double deltaY = Math.Sqrt(Math.Max(0, length * length - (double)deltaX * deltaX));
// When `deltaY` is small, one segment is always enough.
@@ -280,59 +284,38 @@ namespace osu.Game.Rulesets.Catch.Objects
{
Vector2 nextPosition = new Vector2(nextX, nextY);
sliderPath.ControlPoints.Add(new PathControlPoint(nextPosition));
- currentDistance += Vector2.Distance(lastPosition, nextPosition);
+ currentTime += Vector2.Distance(lastPosition, nextPosition) / velocity;
lastPosition = nextPosition;
}
}
///
- /// Find the index at which a new vertex with can be inserted.
+ /// Find the index at which a new vertex with can be inserted.
///
- private int vertexIndexAtDistance(double distance)
+ private int vertexIndexAtTime(double time)
{
- // The position of `(distance, Infinity)` is uniquely determined because infinite positions are not allowed.
- int i = vertices.BinarySearch(new JuiceStreamPathVertex(distance, float.PositiveInfinity));
+ // The position of `(time, Infinity)` is uniquely determined because infinite positions are not allowed.
+ int i = vertices.BinarySearch(new JuiceStreamPathVertex(time, float.PositiveInfinity));
return i < 0 ? ~i : i;
}
///
- /// Compute the position at the given , assuming is the vertex index returned by .
+ /// Compute the position at the given , assuming is the vertex index returned by .
///
- private float positionAtDistance(double distance, int index)
+ private float positionAtTime(double time, int index)
{
if (index <= 0)
return vertices[0].X;
if (index >= vertices.Count)
return vertices[^1].X;
- double length = vertices[index].Distance - vertices[index - 1].Distance;
- if (Precision.AlmostEquals(length, 0))
+ double duration = vertices[index].Time - vertices[index - 1].Time;
+ if (Precision.AlmostEquals(duration, 0))
return vertices[index].X;
float deltaX = vertices[index].X - vertices[index - 1].X;
- return (float)(vertices[index - 1].X + deltaX * ((distance - vertices[index - 1].Distance) / length));
- }
-
- ///
- /// Check the two vertices can connected directly while satisfying the slope condition.
- ///
- private bool canConnect(JuiceStreamPathVertex vertex1, JuiceStreamPathVertex vertex2, float allowance = 0)
- {
- double xDistance = Math.Abs((double)vertex2.X - vertex1.X);
- float length = (float)Math.Abs(vertex2.Distance - vertex1.Distance);
- return xDistance <= length + allowance;
- }
-
- ///
- /// Move the position of towards the position of
- /// until the vertex pair satisfies the condition .
- ///
- /// The resulting position of .
- private float clampToConnectablePosition(JuiceStreamPathVertex fixedVertex, JuiceStreamPathVertex movableVertex)
- {
- float length = (float)Math.Abs(movableVertex.Distance - fixedVertex.Distance);
- return Math.Clamp(movableVertex.X, fixedVertex.X - length, fixedVertex.X + length);
+ return (float)(vertices[index - 1].X + deltaX * ((time - vertices[index - 1].Time) / duration));
}
private void invalidate() => InvalidationID++;
diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStreamPathVertex.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStreamPathVertex.cs
index 58c50603c4..afef2e637f 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStreamPathVertex.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStreamPathVertex.cs
@@ -12,22 +12,22 @@ namespace osu.Game.Rulesets.Catch.Objects
///
public readonly struct JuiceStreamPathVertex : IComparable
{
- public readonly double Distance;
+ public readonly double Time;
public readonly float X;
- public JuiceStreamPathVertex(double distance, float x)
+ public JuiceStreamPathVertex(double time, float x)
{
- Distance = distance;
+ Time = time;
X = x;
}
public int CompareTo(JuiceStreamPathVertex other)
{
- int c = Distance.CompareTo(other.Distance);
+ int c = Time.CompareTo(other.Time);
return c != 0 ? c : X.CompareTo(other.X);
}
- public override string ToString() => $"({Distance}, {X})";
+ public override string ToString() => $"({Time}, {X})";
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist b/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist
index 78349334b4..82d1c8ea24 100644
--- a/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist
+++ b/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist
@@ -37,6 +37,8 @@
XSAppIconAssets
Assets.xcassets/AppIcon.appiconset
+ UIApplicationSupportsIndirectInputEvents
+
CADisableMinimumFrameDurationOnPhone
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
index 4bb049b1a4..6130a80bb4 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
@@ -97,12 +97,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
set => InternalChild = value;
}
- public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
- {
- throw new System.NotImplementedException();
- }
-
- public override SnapResult FindSnappedPosition(Vector2 screenSpacePosition)
+ public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
{
throw new System.NotImplementedException();
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
index 8f25668dd0..7a99565e8a 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
@@ -5,7 +5,10 @@ using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mania.Skinning.Default;
using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
@@ -52,8 +55,29 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
base.UpdateTimeAndPosition(result);
- if (PlacementActive == PlacementState.Waiting)
- Column = result.Playfield as Column;
+ if (result.Playfield is Column col)
+ {
+ // Apply an offset to better align with the visual grid.
+ // This should only be applied during placement, as during selection / drag operations the movement is relative
+ // to the initial point of interaction rather than the grid.
+ switch (col.ScrollingInfo.Direction.Value)
+ {
+ case ScrollingDirection.Down:
+ result.ScreenSpacePosition -= new Vector2(0, getNoteHeight(col) / 2);
+ break;
+
+ case ScrollingDirection.Up:
+ result.ScreenSpacePosition += new Vector2(0, getNoteHeight(col) / 2);
+ break;
+ }
+
+ if (PlacementActive == PlacementState.Waiting)
+ Column = col;
+ }
}
+
+ private float getNoteHeight(Column resultPlayfield) =>
+ resultPlayfield.ToScreenSpace(new Vector2(DefaultNotePiece.NOTE_HEIGHT)).Y -
+ resultPlayfield.ToScreenSpace(Vector2.Zero).Y;
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
index fef315e2ef..c389e1bced 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
@@ -1,15 +1,14 @@
// 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.Rulesets.Edit;
-using osu.Game.Rulesets.Edit.Tools;
-using osu.Game.Rulesets.Mania.Objects;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Input;
-using osu.Game.Rulesets.Mania.Skinning.Default;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Tools;
+using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
@@ -56,28 +55,6 @@ namespace osu.Game.Rulesets.Mania.Edit
protected override Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) =>
Playfield.GetColumnByPosition(screenSpacePosition);
- public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
- {
- var result = base.FindSnappedPositionAndTime(screenSpacePosition);
-
- switch (ScrollingInfo.Direction.Value)
- {
- case ScrollingDirection.Down:
- result.ScreenSpacePosition -= new Vector2(0, getNoteHeight() / 2);
- break;
-
- case ScrollingDirection.Up:
- result.ScreenSpacePosition += new Vector2(0, getNoteHeight() / 2);
- break;
- }
-
- return result;
- }
-
- private float getNoteHeight() =>
- Playfield.GetColumn(0).ToScreenSpace(new Vector2(DefaultNotePiece.NOTE_HEIGHT)).Y -
- Playfield.GetColumn(0).ToScreenSpace(Vector2.Zero).Y;
-
protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null)
{
drawableRuleset = new DrawableManiaEditorRuleset(ruleset, beatmap, mods);
diff --git a/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist b/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist
index b9f371c049..a88b74695c 100644
--- a/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist
+++ b/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist
@@ -37,6 +37,8 @@
XSAppIconAssets
Assets.xcassets/AppIcon.appiconset
+ UIApplicationSupportsIndirectInputEvents
+
CADisableMinimumFrameDurationOnPhone
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
index 02beb0f2a4..b0d6170190 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
@@ -7,6 +7,7 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
+using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
@@ -123,33 +124,27 @@ namespace osu.Game.Rulesets.Osu.Edit
}
}
- public override SnapResult FindSnappedPosition(Vector2 screenSpacePosition)
+ public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
{
- if (snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
+ if (snapType.HasFlagFast(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
return snapResult;
- return new SnapResult(screenSpacePosition, null);
- }
-
- public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
- {
- var positionSnap = FindSnappedPosition(screenSpacePosition);
- if (positionSnap.ScreenSpacePosition != screenSpacePosition)
- return positionSnap;
-
- if (distanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
+ if (snapType.HasFlagFast(SnapType.Grids))
{
- (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
- return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition));
+ if (distanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
+ {
+ (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
+ return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition));
+ }
+
+ if (rectangularGridSnapToggle.Value == TernaryState.True)
+ {
+ Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(screenSpacePosition));
+ return new SnapResult(rectangularPositionSnapGrid.ToScreenSpace(pos), null, PlayfieldAtScreenSpacePosition(screenSpacePosition));
+ }
}
- if (rectangularGridSnapToggle.Value == TernaryState.True)
- {
- Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(screenSpacePosition));
- return new SnapResult(rectangularPositionSnapGrid.ToScreenSpace(pos), null, PlayfieldAtScreenSpacePosition(screenSpacePosition));
- }
-
- return base.FindSnappedPositionAndTime(screenSpacePosition);
+ return base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
}
private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult)
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs
index 391147648f..dd6226e19b 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs
@@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
private readonly Bindable accentColour = new Bindable();
private readonly IBindable indexInCurrentCombo = new Bindable();
- [Resolved(canBeNull: true)]
+ [Resolved(canBeNull: true)] // Can't really be null but required to handle potential of disposal before DI completes.
private DrawableHitObject? drawableObject { get; set; }
[Resolved]
diff --git a/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist b/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist
index 65c47d2115..9628475b3e 100644
--- a/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist
+++ b/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist
@@ -37,6 +37,8 @@
XSAppIconAssets
Assets.xcassets/AppIcon.appiconset
+ UIApplicationSupportsIndirectInputEvents
+
CADisableMinimumFrameDurationOnPhone
diff --git a/osu.Game.Tests.iOS/Info.plist b/osu.Game.Tests.iOS/Info.plist
index ed0c2e4dbf..31e2b3f257 100644
--- a/osu.Game.Tests.iOS/Info.plist
+++ b/osu.Game.Tests.iOS/Info.plist
@@ -37,6 +37,8 @@
XSAppIconAssets
Assets.xcassets/AppIcon.appiconset
+ UIApplicationSupportsIndirectInputEvents
+
CADisableMinimumFrameDurationOnPhone
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
index 468cb7683c..89baaf228d 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
@@ -863,5 +863,40 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(decoded.Difficulty.OverallDifficulty, Is.EqualTo(1));
}
}
+
+ [Test]
+ public void TestLegacyAdjacentImplicitCatmullSegmentsAreMerged()
+ {
+ var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
+
+ using (var resStream = TestResources.OpenResource("adjacent-catmull-segments.osu"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var decoded = decoder.Decode(stream);
+ var controlPoints = ((IHasPath)decoded.HitObjects[0]).Path.ControlPoints;
+
+ Assert.That(controlPoints.Count, Is.EqualTo(6));
+ Assert.That(controlPoints.Single(c => c.Type != null).Type, Is.EqualTo(PathType.Catmull));
+ }
+ }
+
+ [Test]
+ public void TestNonLegacyAdjacentImplicitCatmullSegmentsAreNotMerged()
+ {
+ var decoder = new LegacyBeatmapDecoder(LegacyBeatmapEncoder.FIRST_LAZER_VERSION) { ApplyOffsets = false };
+
+ using (var resStream = TestResources.OpenResource("adjacent-catmull-segments.osu"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var decoded = decoder.Decode(stream);
+ var controlPoints = ((IHasPath)decoded.HitObjects[0]).Path.ControlPoints;
+
+ Assert.That(controlPoints.Count, Is.EqualTo(4));
+ Assert.That(controlPoints[0].Type, Is.EqualTo(PathType.Catmull));
+ Assert.That(controlPoints[1].Type, Is.EqualTo(PathType.Catmull));
+ Assert.That(controlPoints[2].Type, Is.EqualTo(PathType.Catmull));
+ Assert.That(controlPoints[3].Type, Is.Null);
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Resources/adjacent-catmull-segments.osu b/osu.Game.Tests/Resources/adjacent-catmull-segments.osu
new file mode 100644
index 0000000000..a436fe5228
--- /dev/null
+++ b/osu.Game.Tests/Resources/adjacent-catmull-segments.osu
@@ -0,0 +1,2 @@
+[HitObjects]
+200,304,23875,6,0,C|288:304|288:304|288:208|288:208|352:208,1,260,8|0
\ No newline at end of file
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
index 3aa3481cbf..ef07c3e411 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
@@ -185,10 +185,7 @@ namespace osu.Game.Tests.Visual.Editing
private class SnapProvider : IDistanceSnapProvider
{
- public SnapResult FindSnappedPosition(Vector2 screenSpacePosition) =>
- new SnapResult(screenSpacePosition, null);
-
- public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
+ public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.Grids) => new SnapResult(screenSpacePosition, 0);
public Bindable DistanceSpacingMultiplier { get; } = new BindableDouble(1);
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs
index bf1767cc96..3e3c8122b3 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs
@@ -5,6 +5,8 @@ using System;
using System.Linq;
using System.Collections.Generic;
using System.Net;
+using System.Threading;
+using JetBrains.Annotations;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -31,7 +33,7 @@ namespace osu.Game.Tests.Visual.Online
[TestFixture]
public class TestSceneChatOverlayV2 : OsuManualInputManagerTestScene
{
- private ChatOverlayV2 chatOverlay;
+ private TestChatOverlayV2 chatOverlay;
private ChannelManager channelManager;
private APIUser testUser;
@@ -61,7 +63,7 @@ namespace osu.Game.Tests.Visual.Online
Children = new Drawable[]
{
channelManager,
- chatOverlay = new ChatOverlayV2 { RelativeSizeAxes = Axes.Both },
+ chatOverlay = new TestChatOverlayV2 { RelativeSizeAxes = Axes.Both },
},
};
});
@@ -365,19 +367,19 @@ namespace osu.Game.Tests.Visual.Online
}
[Test]
- public void TextBoxRetainsFocus()
+ public void TestTextBoxRetainsFocus()
{
AddStep("Show overlay", () => chatOverlay.Show());
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
+ AddStep("Click drawable channel", () => clickDrawable(currentDrawableChannel));
+ AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
AddStep("Click selector", () => clickDrawable(chatOverlay.ChildrenOfType().Single()));
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
AddStep("Click listing", () => clickDrawable(chatOverlay.ChildrenOfType().Single()));
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
- AddStep("Click drawable channel", () => clickDrawable(currentDrawableChannel));
- AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
AddStep("Click channel list", () => clickDrawable(chatOverlay.ChildrenOfType().Single()));
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
AddStep("Click top bar", () => clickDrawable(chatOverlay.ChildrenOfType().Single()));
@@ -386,6 +388,34 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("TextBox is not focused", () => InputManager.FocusedDrawable == null);
}
+ [Test]
+ public void TestSlowLoadingChannel()
+ {
+ AddStep("Show overlay (slow-loading)", () =>
+ {
+ chatOverlay.Show();
+ chatOverlay.SlowLoading = true;
+ });
+ AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
+ AddAssert("Channel 1 loading", () => !channelIsVisible && chatOverlay.GetSlowLoadingChannel(testChannel1).LoadState == LoadState.Loading);
+
+ AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2));
+ AddStep("Select channel 2", () => clickDrawable(getChannelListItem(testChannel2)));
+ AddAssert("Channel 2 loading", () => !channelIsVisible && chatOverlay.GetSlowLoadingChannel(testChannel2).LoadState == LoadState.Loading);
+
+ AddStep("Finish channel 1 load", () => chatOverlay.GetSlowLoadingChannel(testChannel1).LoadEvent.Set());
+ AddAssert("Channel 1 ready", () => chatOverlay.GetSlowLoadingChannel(testChannel1).LoadState == LoadState.Ready);
+ AddAssert("Channel 1 not displayed", () => !channelIsVisible);
+
+ AddStep("Finish channel 2 load", () => chatOverlay.GetSlowLoadingChannel(testChannel2).LoadEvent.Set());
+ AddAssert("Channel 2 loaded", () => chatOverlay.GetSlowLoadingChannel(testChannel2).IsLoaded);
+ AddAssert("Channel 2 displayed", () => channelIsVisible && currentDrawableChannel.Channel == testChannel2);
+
+ AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
+ AddAssert("Channel 1 loaded", () => chatOverlay.GetSlowLoadingChannel(testChannel1).IsLoaded);
+ AddAssert("Channel 1 displayed", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
+ }
+
private bool listingIsVisible =>
chatOverlay.ChildrenOfType().Single().State.Value == Visibility.Visible;
@@ -432,5 +462,35 @@ namespace osu.Game.Tests.Visual.Online
Topic = $"We talk about the number {id} here",
Type = ChannelType.Public,
};
+
+ private class TestChatOverlayV2 : ChatOverlayV2
+ {
+ public bool SlowLoading { get; set; }
+
+ public SlowLoadingDrawableChannel GetSlowLoadingChannel(Channel channel) => DrawableChannels.OfType().Single(c => c.Channel == channel);
+
+ protected override ChatOverlayDrawableChannel CreateDrawableChannel(Channel newChannel)
+ {
+ return SlowLoading
+ ? new SlowLoadingDrawableChannel(newChannel)
+ : new ChatOverlayDrawableChannel(newChannel);
+ }
+ }
+
+ private class SlowLoadingDrawableChannel : ChatOverlayDrawableChannel
+ {
+ public readonly ManualResetEventSlim LoadEvent = new ManualResetEventSlim();
+
+ public SlowLoadingDrawableChannel([NotNull] Channel channel)
+ : base(channel)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ LoadEvent.Wait(10000);
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenImportFromStable.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenImportFromStable.cs
new file mode 100644
index 0000000000..081b240795
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenImportFromStable.cs
@@ -0,0 +1,48 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Threading;
+using System.Threading.Tasks;
+using Moq;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Screens;
+using osu.Framework.Utils;
+using osu.Game.Database;
+using osu.Game.Overlays;
+using osu.Game.Overlays.FirstRunSetup;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public class TestSceneFirstRunScreenImportFromStable : OsuManualInputManagerTestScene
+ {
+ [Cached]
+ private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
+
+ private readonly Mock legacyImportManager = new Mock();
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ legacyImportManager.Setup(m => m.GetImportCount(It.IsAny(), It.IsAny())).Returns(() => Task.FromResult(RNG.Next(0, 256)));
+
+ Dependencies.CacheAs(legacyImportManager.Object);
+ }
+
+ public TestSceneFirstRunScreenImportFromStable()
+ {
+ AddStep("load screen", () =>
+ {
+ Child = new PopoverContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new ScreenStack(new ScreenImportFromStable())
+ }
+ };
+ });
+ }
+ }
+}
diff --git a/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs b/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs
index df0a69cb25..84903d381a 100644
--- a/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs
+++ b/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs
@@ -3,9 +3,11 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using osu.Framework.Allocation;
+using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Database;
using osu.Game.Online;
@@ -89,6 +91,8 @@ namespace osu.Game.Beatmaps.Drawables
private void queueDownloads(string[] sourceFilenames, int? limit = null)
{
+ Debug.Assert(LoadState == LoadState.NotLoaded);
+
try
{
// Matches osu-stable, in order to provide new users with roughly the same randomised selection of bundled beatmaps.
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
index b91a74c4a1..3b4200e7a9 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Beatmaps.Formats
{
public class LegacyBeatmapEncoder
{
- public const int LATEST_VERSION = 128;
+ public const int FIRST_LAZER_VERSION = 128;
///
/// osu! is generally slower than taiko, so a factor is added to increase
@@ -55,7 +55,7 @@ namespace osu.Game.Beatmaps.Formats
public void Encode(TextWriter writer)
{
- writer.WriteLine($"osu file format v{LATEST_VERSION}");
+ writer.WriteLine($"osu file format v{FIRST_LAZER_VERSION}");
writer.WriteLine();
handleGeneral(writer);
diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs
index 5845e0d4d1..700b0f5dcb 100644
--- a/osu.Game/Collections/CollectionManager.cs
+++ b/osu.Game/Collections/CollectionManager.cs
@@ -111,6 +111,18 @@ namespace osu.Game.Collections
public Action PostNotification { protected get; set; }
+ public Task GetAvailableCount(StableStorage stableStorage)
+ {
+ if (!stableStorage.Exists(database_name))
+ return Task.FromResult(0);
+
+ return Task.Run(() =>
+ {
+ using (var stream = stableStorage.GetStream(database_name))
+ return readCollections(stream).Count;
+ });
+ }
+
///
/// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future.
///
diff --git a/osu.Game/Database/LegacyImportManager.cs b/osu.Game/Database/LegacyImportManager.cs
index 59394c2952..af9db1b6ec 100644
--- a/osu.Game/Database/LegacyImportManager.cs
+++ b/osu.Game/Database/LegacyImportManager.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Threading;
using System.Threading.Tasks;
using osu.Framework;
using osu.Framework.Allocation;
@@ -36,22 +37,66 @@ namespace osu.Game.Database
[Resolved]
private CollectionManager collections { get; set; }
- [Resolved]
+ [Resolved(canBeNull: true)]
private OsuGame game { get; set; }
[Resolved]
private IDialogOverlay dialogOverlay { get; set; }
- [Resolved(CanBeNull = true)]
+ [Resolved(canBeNull: true)]
private DesktopGameHost desktopGameHost { get; set; }
private StableStorage cachedStorage;
public bool SupportsImportFromStable => RuntimeInfo.IsDesktop;
- public async Task ImportFromStableAsync(StableContent content)
+ public void UpdateStorage(string stablePath) => cachedStorage = new StableStorage(stablePath, desktopGameHost);
+
+ public virtual async Task GetImportCount(StableContent content, CancellationToken cancellationToken)
{
- var stableStorage = await getStableStorage().ConfigureAwait(false);
+ var stableStorage = GetCurrentStableStorage();
+
+ if (stableStorage == null)
+ return 0;
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ switch (content)
+ {
+ case StableContent.Beatmaps:
+ return await new LegacyBeatmapImporter(beatmaps).GetAvailableCount(stableStorage);
+
+ case StableContent.Skins:
+ return await new LegacySkinImporter(skins).GetAvailableCount(stableStorage);
+
+ case StableContent.Collections:
+ return await collections.GetAvailableCount(stableStorage);
+
+ case StableContent.Scores:
+ return await new LegacyScoreImporter(scores).GetAvailableCount(stableStorage);
+
+ default:
+ throw new ArgumentException($"Only one {nameof(StableContent)} flag should be specified.");
+ }
+ }
+
+ public async Task ImportFromStableAsync(StableContent content, bool interactiveLocateIfNotFound = true)
+ {
+ var stableStorage = GetCurrentStableStorage();
+
+ if (stableStorage == null)
+ {
+ if (!interactiveLocateIfNotFound)
+ return;
+
+ var taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ Schedule(() => dialogOverlay.Push(new StableDirectoryLocationDialog(taskCompletionSource)));
+ string stablePath = await taskCompletionSource.Task.ConfigureAwait(false);
+
+ UpdateStorage(stablePath);
+ stableStorage = GetCurrentStableStorage();
+ }
+
var importTasks = new List();
Task beatmapImportTask = Task.CompletedTask;
@@ -70,20 +115,16 @@ namespace osu.Game.Database
await Task.WhenAll(importTasks.ToArray()).ConfigureAwait(false);
}
- private async Task getStableStorage()
+ public StableStorage GetCurrentStableStorage()
{
if (cachedStorage != null)
return cachedStorage;
- var stableStorage = game.GetStorageForStableInstall();
+ var stableStorage = game?.GetStorageForStableInstall();
if (stableStorage != null)
return cachedStorage = stableStorage;
- var taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
- Schedule(() => dialogOverlay.Push(new StableDirectoryLocationDialog(taskCompletionSource)));
- string stablePath = await taskCompletionSource.Task.ConfigureAwait(false);
-
- return cachedStorage = new StableStorage(stablePath, desktopGameHost);
+ return null;
}
}
diff --git a/osu.Game/Database/LegacyModelImporter.cs b/osu.Game/Database/LegacyModelImporter.cs
index d85fb5aab2..9b2a54dada 100644
--- a/osu.Game/Database/LegacyModelImporter.cs
+++ b/osu.Game/Database/LegacyModelImporter.cs
@@ -24,8 +24,14 @@ namespace osu.Game.Database
///
/// Select paths to import from stable where all paths should be absolute. Default implementation iterates all directories in .
///
- protected virtual IEnumerable GetStableImportPaths(Storage storage) => storage.GetDirectories(ImportFromStablePath)
- .Select(path => storage.GetFullPath(path));
+ protected virtual IEnumerable GetStableImportPaths(Storage storage)
+ {
+ if (!storage.ExistsDirectory(ImportFromStablePath))
+ return Enumerable.Empty();
+
+ return storage.GetDirectories(ImportFromStablePath)
+ .Select(path => storage.GetFullPath(path));
+ }
protected readonly IModelImporter Importer;
@@ -34,6 +40,8 @@ namespace osu.Game.Database
Importer = importer;
}
+ public Task GetAvailableCount(StableStorage stableStorage) => Task.Run(() => GetStableImportPaths(PrepareStableStorage(stableStorage)).Count());
+
public Task ImportFromStableAsync(StableStorage stableStorage)
{
var storage = PrepareStableStorage(stableStorage);
diff --git a/osu.Game/Database/LegacyScoreImporter.cs b/osu.Game/Database/LegacyScoreImporter.cs
index 48445b7bdb..131b4ffb0e 100644
--- a/osu.Game/Database/LegacyScoreImporter.cs
+++ b/osu.Game/Database/LegacyScoreImporter.cs
@@ -15,8 +15,14 @@ namespace osu.Game.Database
protected override string ImportFromStablePath => Path.Combine("Data", "r");
protected override IEnumerable GetStableImportPaths(Storage storage)
- => storage.GetFiles(ImportFromStablePath).Where(p => Importer.HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false))
- .Select(path => storage.GetFullPath(path));
+ {
+ if (!storage.ExistsDirectory(ImportFromStablePath))
+ return Enumerable.Empty();
+
+ return storage.GetFiles(ImportFromStablePath)
+ .Where(p => Importer.HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false))
+ .Select(path => storage.GetFullPath(path));
+ }
public LegacyScoreImporter(IModelImporter importer)
: base(importer)
diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs
index fd64cc2056..82b9fe559f 100644
--- a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs
@@ -6,6 +6,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Graphics.UserInterfaceV2
@@ -25,7 +26,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
set => Component.ReadOnly = value;
}
- public string PlaceholderText
+ public LocalisableString PlaceholderText
{
set => Component.PlaceholderText = value;
}
diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs
index ce2e7794a9..456bde6d1b 100644
--- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs
@@ -10,6 +10,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays;
namespace osu.Game.Graphics.UserInterfaceV2
{
@@ -44,8 +45,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
internal class Background : CompositeDrawable
{
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ [BackgroundDependencyLoader(true)]
+ private void load(OverlayColourProvider overlayColourProvider, OsuColour colours)
{
RelativeSizeAxes = Axes.Both;
@@ -54,7 +55,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
InternalChild = new Box
{
- Colour = colours.GreySeaFoamDarker,
+ Colour = overlayColourProvider?.Background5 ?? colours.GreySeaFoamDarker,
RelativeSizeAxes = Axes.Both,
};
}
diff --git a/osu.Game/Localisation/CommonStrings.cs b/osu.Game/Localisation/CommonStrings.cs
index 9cd626af0f..1fd677034d 100644
--- a/osu.Game/Localisation/CommonStrings.cs
+++ b/osu.Game/Localisation/CommonStrings.cs
@@ -69,6 +69,26 @@ namespace osu.Game.Localisation
///
public static LocalisableString SelectAll => new TranslatableString(getKey(@"select_all"), @"Select All");
+ ///
+ /// "Beatmaps"
+ ///
+ public static LocalisableString Beatmaps => new TranslatableString(getKey(@"beatmaps"), @"Beatmaps");
+
+ ///
+ /// "Scores"
+ ///
+ public static LocalisableString Scores => new TranslatableString(getKey(@"scores"), @"Scores");
+
+ ///
+ /// "Skins"
+ ///
+ public static LocalisableString Skins => new TranslatableString(getKey(@"skins"), @"Skins");
+
+ ///
+ /// "Collections"
+ ///
+ public static LocalisableString Collections => new TranslatableString(getKey(@"collections"), @"Collections");
+
private static string getKey(string key) => $@"{prefix}:{key}";
}
-}
+}
\ No newline at end of file
diff --git a/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs b/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs
new file mode 100644
index 0000000000..deac7d8628
--- /dev/null
+++ b/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs
@@ -0,0 +1,56 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Localisation;
+
+namespace osu.Game.Localisation
+{
+ public static class FirstRunOverlayImportFromStableScreenStrings
+ {
+ private const string prefix = @"osu.Game.Resources.Localisation.ScreenImportFromStable";
+
+ ///
+ /// "Import"
+ ///
+ public static LocalisableString Header => new TranslatableString(getKey(@"header"), @"Import");
+
+ ///
+ /// "If you have an installation of a previous osu! version, you can choose to migrate your existing content. Note that this will create a copy, and not affect your existing installation."
+ ///
+ public static LocalisableString Description => new TranslatableString(getKey(@"description"),
+ @"If you have an installation of a previous osu! version, you can choose to migrate your existing content. Note that this will create a copy, and not affect your existing installation.");
+
+ ///
+ /// "previous osu! install"
+ ///
+ public static LocalisableString LocateDirectoryLabel => new TranslatableString(getKey(@"locate_directory_label"), @"previous osu! install");
+
+ ///
+ /// "Click to locate a previous osu! install"
+ ///
+ public static LocalisableString LocateDirectoryPlaceholder => new TranslatableString(getKey(@"locate_directory_placeholder"), @"Click to locate a previous osu! install");
+
+ ///
+ /// "Import content from previous version"
+ ///
+ public static LocalisableString ImportButton => new TranslatableString(getKey(@"import_button"), @"Import content from previous version");
+
+ ///
+ /// "Your import will continue in the background. Check on its progress in the notifications sidebar!"
+ ///
+ public static LocalisableString ImportInProgress =>
+ new TranslatableString(getKey(@"import_in_progress"), @"Your import will continue in the background. Check on its progress in the notifications sidebar!");
+
+ ///
+ /// "calculating..."
+ ///
+ public static LocalisableString Calculating => new TranslatableString(getKey(@"calculating"), @"calculating...");
+
+ ///
+ /// "{0} items"
+ ///
+ public static LocalisableString Items(int arg0) => new TranslatableString(getKey(@"items"), @"{0} item(s)", arg0);
+
+ private static string getKey(string key) => $@"{prefix}:{key}";
+ }
+}
diff --git a/osu.Game/Overlays/ChatOverlayV2.cs b/osu.Game/Overlays/ChatOverlayV2.cs
index 6eec0bbbf4..b2c1f6858c 100644
--- a/osu.Game/Overlays/ChatOverlayV2.cs
+++ b/osu.Game/Overlays/ChatOverlayV2.cs
@@ -39,8 +39,11 @@ namespace osu.Game.Overlays
private ChatTextBar textBar = null!;
private Container currentChannelContainer = null!;
- private readonly BindableFloat chatHeight = new BindableFloat();
+ private readonly Dictionary loadedChannels = new Dictionary();
+ protected IEnumerable DrawableChannels => loadedChannels.Values;
+
+ private readonly BindableFloat chatHeight = new BindableFloat();
private bool isDraggingTopBar;
private float dragStartChatHeight;
@@ -173,7 +176,7 @@ namespace osu.Game.Overlays
if (currentChannel.Value?.Id != channel.Id)
{
if (!channel.Joined.Value)
- channel = channelManager.JoinChannel(channel);
+ channel = channelManager.JoinChannel(channel, false);
channelManager.CurrentChannel.Value = channel;
}
@@ -240,38 +243,76 @@ namespace osu.Game.Overlays
if (newChannel == null)
{
// null channel denotes that we should be showing the listing.
- channelListing.State.Value = Visibility.Visible;
+ currentChannelContainer.Clear(false);
+ channelListing.Show();
textBar.ShowSearch.Value = true;
}
else
{
- channelListing.State.Value = Visibility.Hidden;
+ channelListing.Hide();
textBar.ShowSearch.Value = false;
- loading.Show();
- LoadComponentAsync(new ChatOverlayDrawableChannel(newChannel), loaded =>
+ if (loadedChannels.ContainsKey(newChannel))
{
- currentChannelContainer.Clear();
- currentChannelContainer.Add(loaded);
- loading.Hide();
- });
+ currentChannelContainer.Clear(false);
+ currentChannelContainer.Add(loadedChannels[newChannel]);
+ }
+ else
+ {
+ loading.Show();
+
+ // Ensure the drawable channel is stored before async load to prevent double loading
+ ChatOverlayDrawableChannel drawableChannel = CreateDrawableChannel(newChannel);
+ loadedChannels.Add(newChannel, drawableChannel);
+
+ LoadComponentAsync(drawableChannel, loadedDrawable =>
+ {
+ // Ensure the current channel hasn't changed by the time the load completes
+ if (currentChannel.Value != loadedDrawable.Channel)
+ return;
+
+ // Ensure the cached reference hasn't been removed from leaving the channel
+ if (!loadedChannels.ContainsKey(loadedDrawable.Channel))
+ return;
+
+ currentChannelContainer.Clear(false);
+ currentChannelContainer.Add(loadedDrawable);
+ loading.Hide();
+ });
+ }
}
}
+ protected virtual ChatOverlayDrawableChannel CreateDrawableChannel(Channel newChannel) => new ChatOverlayDrawableChannel(newChannel);
+
private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args)
{
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
IEnumerable joinedChannels = filterChannels(args.NewItems);
+
foreach (var channel in joinedChannels)
channelList.AddChannel(channel);
+
break;
case NotifyCollectionChangedAction.Remove:
IEnumerable leftChannels = filterChannels(args.OldItems);
+
foreach (var channel in leftChannels)
+ {
channelList.RemoveChannel(channel);
+
+ if (loadedChannels.ContainsKey(channel))
+ {
+ ChatOverlayDrawableChannel loaded = loadedChannels[channel];
+ loadedChannels.Remove(channel);
+ // DrawableChannel removed from cache must be manually disposed
+ loaded.Dispose();
+ }
+ }
+
break;
}
}
diff --git a/osu.Game/Overlays/FirstRunSetup/FirstRunSetupScreen.cs b/osu.Game/Overlays/FirstRunSetup/FirstRunSetupScreen.cs
index d1ea91e51a..b043f05bd8 100644
--- a/osu.Game/Overlays/FirstRunSetup/FirstRunSetupScreen.cs
+++ b/osu.Game/Overlays/FirstRunSetup/FirstRunSetupScreen.cs
@@ -21,6 +21,8 @@ namespace osu.Game.Overlays.FirstRunSetup
protected const float CONTENT_FONT_SIZE = 16;
+ protected const float CONTENT_PADDING = 30;
+
protected const float HEADER_FONT_SIZE = 24;
[Resolved]
@@ -41,7 +43,7 @@ namespace osu.Game.Overlays.FirstRunSetup
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
- Padding = new MarginPadding { Horizontal = 30 },
+ Padding = new MarginPadding { Horizontal = CONTENT_PADDING },
Children = new Drawable[]
{
new OsuSpriteText
diff --git a/osu.Game/Overlays/FirstRunSetup/ProgressRoundedButton.cs b/osu.Game/Overlays/FirstRunSetup/ProgressRoundedButton.cs
new file mode 100644
index 0000000000..ee2db1f3d4
--- /dev/null
+++ b/osu.Game/Overlays/FirstRunSetup/ProgressRoundedButton.cs
@@ -0,0 +1,103 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+#nullable enable
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Graphics.UserInterfaceV2;
+using osuTK;
+
+namespace osu.Game.Overlays.FirstRunSetup
+{
+ public class ProgressRoundedButton : RoundedButton
+ {
+ public new Action? Action;
+
+ [Resolved]
+ private OsuColour colours { get; set; } = null!;
+
+ private ProgressBar progressBar = null!;
+
+ private LoadingSpinner loading = null!;
+
+ private SpriteIcon tick = null!;
+
+ public ProgressRoundedButton()
+ {
+ base.Action = () =>
+ {
+ loading.Show();
+ Enabled.Value = false;
+
+ Action?.Invoke();
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ AddRange(new Drawable[]
+ {
+ progressBar = new ProgressBar(false)
+ {
+ RelativeSizeAxes = Axes.Both,
+ Blending = BlendingParameters.Additive,
+ FillColour = BackgroundColour,
+ Alpha = 0.5f,
+ Depth = float.MinValue
+ },
+ new Container
+ {
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ Margin = new MarginPadding(15),
+ Size = new Vector2(20),
+ Children = new Drawable[]
+ {
+ loading = new LoadingSpinner
+ {
+ RelativeSizeAxes = Axes.Both,
+ Size = Vector2.One,
+ },
+ tick = new SpriteIcon
+ {
+ Icon = FontAwesome.Solid.Check,
+ RelativeSizeAxes = Axes.Both,
+ Size = Vector2.One,
+ Alpha = 0,
+ }
+ }
+ },
+ });
+ }
+
+ public void Complete()
+ {
+ loading.Hide();
+ tick.FadeIn(500, Easing.OutQuint);
+
+ Background.FadeColour(colours.Green, 500, Easing.OutQuint);
+ progressBar.FillColour = colours.Green;
+
+ this.TransformBindableTo(progressBar.Current, 1, 500, Easing.OutQuint);
+ }
+
+ public void Abort()
+ {
+ loading.Hide();
+ Enabled.Value = true;
+ this.TransformBindableTo(progressBar.Current, 0, 500, Easing.OutQuint);
+ }
+
+ public void SetProgress(double progress, bool animated)
+ {
+ this.TransformBindableTo(progressBar.Current, progress, animated ? 500 : 0, Easing.OutQuint);
+ }
+ }
+}
diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs b/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs
index 66acdca8c7..17e04c0c99 100644
--- a/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs
+++ b/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs
@@ -14,8 +14,6 @@ using osu.Game.Beatmaps.Drawables;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
-using osu.Game.Graphics.UserInterface;
-using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Online;
using osuTK;
@@ -27,7 +25,6 @@ namespace osu.Game.Overlays.FirstRunSetup
public class ScreenBeatmaps : FirstRunSetupScreen
{
private ProgressRoundedButton downloadBundledButton = null!;
- private ProgressRoundedButton importBeatmapsButton = null!;
private ProgressRoundedButton downloadTutorialButton = null!;
private OsuTextFlowContainer currentlyLoadedBeatmaps = null!;
@@ -43,8 +40,8 @@ namespace osu.Game.Overlays.FirstRunSetup
private IDisposable? beatmapSubscription;
- [BackgroundDependencyLoader(permitNulls: true)]
- private void load(LegacyImportManager? legacyImportManager)
+ [BackgroundDependencyLoader]
+ private void load()
{
Vector2 buttonSize = new Vector2(400, 50);
@@ -106,32 +103,6 @@ namespace osu.Game.Overlays.FirstRunSetup
Action = downloadBundled
},
new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
- {
- Colour = OverlayColourProvider.Content1,
- Text = "If you have an existing osu! install, you can also choose to import your existing beatmap collection.",
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y
- },
- importBeatmapsButton = new ProgressRoundedButton
- {
- Size = buttonSize,
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- BackgroundColour = colours.Blue3,
- Text = MaintenanceSettingsStrings.ImportBeatmapsFromStable,
- Action = () =>
- {
- importBeatmapsButton.Enabled.Value = false;
- legacyImportManager?.ImportFromStableAsync(StableContent.Beatmaps).ContinueWith(t => Schedule(() =>
- {
- if (t.IsCompletedSuccessfully)
- importBeatmapsButton.Complete();
- else
- importBeatmapsButton.Enabled.Value = true;
- }));
- }
- },
- new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
{
Colour = OverlayColourProvider.Content1,
Text = FirstRunSetupBeatmapScreenStrings.ObtainMoreBeatmaps,
@@ -214,45 +185,5 @@ namespace osu.Game.Overlays.FirstRunSetup
downloadBundledButton.SetProgress(progress, true);
}
}
-
- private class ProgressRoundedButton : RoundedButton
- {
- [Resolved]
- private OsuColour colours { get; set; } = null!;
-
- private ProgressBar progressBar = null!;
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- Add(progressBar = new ProgressBar(false)
- {
- RelativeSizeAxes = Axes.Both,
- Blending = BlendingParameters.Additive,
- FillColour = BackgroundColour,
- Alpha = 0.5f,
- Depth = float.MinValue
- });
- }
-
- public void Complete()
- {
- Enabled.Value = false;
-
- Background.FadeColour(colours.Green, 500, Easing.OutQuint);
- progressBar.FillColour = colours.Green;
-
- this.TransformBindableTo(progressBar.Current, 1, 500, Easing.OutQuint);
- }
-
- public void SetProgress(double progress, bool animated)
- {
- if (!Enabled.Value)
- return;
-
- this.TransformBindableTo(progressBar.Current, progress, animated ? 500 : 0, Easing.OutQuint);
- }
- }
}
}
diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs
new file mode 100644
index 0000000000..62b517d982
--- /dev/null
+++ b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs
@@ -0,0 +1,267 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Localisation;
+using osu.Game.Database;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Localisation;
+using osu.Game.Overlays.Settings;
+using osu.Game.Screens.Edit.Setup;
+using osuTK;
+
+namespace osu.Game.Overlays.FirstRunSetup
+{
+ [LocalisableDescription(typeof(FirstRunOverlayImportFromStableScreenStrings), nameof(FirstRunOverlayImportFromStableScreenStrings.Header))]
+ public class ScreenImportFromStable : FirstRunSetupScreen
+ {
+ private static readonly Vector2 button_size = new Vector2(400, 50);
+
+ private ProgressRoundedButton importButton = null!;
+
+ private OsuTextFlowContainer progressText = null!;
+
+ [Resolved]
+ private LegacyImportManager legacyImportManager { get; set; } = null!;
+
+ private StableLocatorLabelledTextBox stableLocatorTextBox = null!;
+
+ private IEnumerable contentCheckboxes => Content.Children.OfType();
+
+ [BackgroundDependencyLoader(permitNulls: true)]
+ private void load()
+ {
+ Content.Children = new Drawable[]
+ {
+ new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
+ {
+ Colour = OverlayColourProvider.Content1,
+ Text = FirstRunOverlayImportFromStableScreenStrings.Description,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y
+ },
+ stableLocatorTextBox = new StableLocatorLabelledTextBox
+ {
+ Label = FirstRunOverlayImportFromStableScreenStrings.LocateDirectoryLabel,
+ PlaceholderText = FirstRunOverlayImportFromStableScreenStrings.LocateDirectoryPlaceholder
+ },
+ new ImportCheckbox(CommonStrings.Beatmaps, StableContent.Beatmaps),
+ new ImportCheckbox(CommonStrings.Scores, StableContent.Scores),
+ new ImportCheckbox(CommonStrings.Skins, StableContent.Skins),
+ new ImportCheckbox(CommonStrings.Collections, StableContent.Collections),
+ importButton = new ProgressRoundedButton
+ {
+ Size = button_size,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Text = FirstRunOverlayImportFromStableScreenStrings.ImportButton,
+ Action = runImport
+ },
+ progressText = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
+ {
+ Colour = OverlayColourProvider.Content1,
+ Text = FirstRunOverlayImportFromStableScreenStrings.ImportInProgress,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Alpha = 0,
+ },
+ };
+
+ stableLocatorTextBox.Current.BindValueChanged(_ => updateStablePath(), true);
+ }
+
+ private void updateStablePath()
+ {
+ var storage = legacyImportManager.GetCurrentStableStorage();
+
+ if (storage == null)
+ {
+ toggleInteraction(false);
+
+ stableLocatorTextBox.Current.Disabled = false;
+ stableLocatorTextBox.Current.Value = string.Empty;
+ return;
+ }
+
+ foreach (var c in contentCheckboxes)
+ {
+ c.Current.Disabled = false;
+ c.UpdateCount();
+ }
+
+ toggleInteraction(true);
+ stableLocatorTextBox.Current.Value = storage.GetFullPath(string.Empty);
+ importButton.Enabled.Value = true;
+ }
+
+ private void runImport()
+ {
+ toggleInteraction(false);
+ progressText.FadeIn(1000, Easing.OutQuint);
+
+ StableContent importableContent = 0;
+
+ foreach (var c in contentCheckboxes.Where(c => c.Current.Value))
+ importableContent |= c.StableContent;
+
+ legacyImportManager.ImportFromStableAsync(importableContent, false).ContinueWith(t => Schedule(() =>
+ {
+ progressText.FadeOut(500, Easing.OutQuint);
+
+ if (t.IsCompletedSuccessfully)
+ importButton.Complete();
+ else
+ {
+ toggleInteraction(true);
+ importButton.Abort();
+ }
+ }));
+ }
+
+ private void toggleInteraction(bool allow)
+ {
+ importButton.Enabled.Value = allow;
+ stableLocatorTextBox.Current.Disabled = !allow;
+ foreach (var c in contentCheckboxes)
+ c.Current.Disabled = !allow;
+ }
+
+ private class ImportCheckbox : SettingsCheckbox
+ {
+ public readonly StableContent StableContent;
+
+ private readonly LocalisableString title;
+
+ [Resolved]
+ private LegacyImportManager legacyImportManager { get; set; } = null!;
+
+ private CancellationTokenSource? countUpdateCancellation;
+
+ public ImportCheckbox(LocalisableString title, StableContent stableContent)
+ {
+ this.title = title;
+
+ StableContent = stableContent;
+
+ Current.Default = true;
+ Current.Value = true;
+
+ LabelText = title;
+ }
+
+ public void UpdateCount()
+ {
+ LabelText = LocalisableString.Interpolate($"{title} ({FirstRunOverlayImportFromStableScreenStrings.Calculating})");
+
+ countUpdateCancellation?.Cancel();
+ countUpdateCancellation = new CancellationTokenSource();
+
+ legacyImportManager.GetImportCount(StableContent, countUpdateCancellation.Token).ContinueWith(task => Schedule(() =>
+ {
+ if (task.IsCanceled)
+ return;
+
+ int count = task.GetResultSafely();
+
+ LabelText = LocalisableString.Interpolate($"{title} ({FirstRunOverlayImportFromStableScreenStrings.Items(count)})");
+ }));
+ }
+ }
+
+ internal class StableLocatorLabelledTextBox : LabelledTextBoxWithPopover, ICanAcceptFiles
+ {
+ [Resolved]
+ private LegacyImportManager legacyImportManager { get; set; } = null!;
+
+ public IEnumerable HandledExtensions { get; } = new[] { string.Empty };
+
+ private readonly Bindable currentDirectory = new Bindable();
+
+ [Resolved(canBeNull: true)] // Can't really be null but required to handle potential of disposal before DI completes.
+ private OsuGameBase? game { get; set; }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ game?.RegisterImportHandler(this);
+
+ currentDirectory.BindValueChanged(onDirectorySelected);
+
+ string? fullPath = legacyImportManager.GetCurrentStableStorage()?.GetFullPath(string.Empty);
+ if (fullPath != null)
+ currentDirectory.Value = new DirectoryInfo(fullPath);
+ }
+
+ private void onDirectorySelected(ValueChangedEvent directory)
+ {
+ if (directory.NewValue == null)
+ {
+ Current.Value = string.Empty;
+ return;
+ }
+
+ // DirectorySelectors can trigger a noop value changed, but `DirectoryInfo` equality doesn't catch this.
+ if (directory.OldValue?.FullName == directory.NewValue.FullName)
+ return;
+
+ if (directory.NewValue?.GetFiles(@"osu!.*.cfg").Any() ?? false)
+ {
+ this.HidePopover();
+
+ string path = directory.NewValue.FullName;
+
+ legacyImportManager.UpdateStorage(path);
+ Current.Value = path;
+ }
+ }
+
+ Task ICanAcceptFiles.Import(params string[] paths)
+ {
+ Schedule(() => currentDirectory.Value = new DirectoryInfo(paths.First()));
+ return Task.CompletedTask;
+ }
+
+ Task ICanAcceptFiles.Import(params ImportTask[] tasks) => throw new NotImplementedException();
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ game?.UnregisterImportHandler(this);
+ }
+
+ public override Popover GetPopover() => new DirectoryChooserPopover(currentDirectory);
+
+ private class DirectoryChooserPopover : OsuPopover
+ {
+ public DirectoryChooserPopover(Bindable currentDirectory)
+ {
+ Child = new Container
+ {
+ Size = new Vector2(600, 400),
+ Child = new OsuDirectorySelector(currentDirectory.Value?.FullName)
+ {
+ RelativeSizeAxes = Axes.Both,
+ CurrentPath = { BindTarget = currentDirectory }
+ },
+ };
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game/Overlays/FirstRunSetupOverlay.cs b/osu.Game/Overlays/FirstRunSetupOverlay.cs
index cebb2f5e3b..7b0de4affe 100644
--- a/osu.Game/Overlays/FirstRunSetupOverlay.cs
+++ b/osu.Game/Overlays/FirstRunSetupOverlay.cs
@@ -4,12 +4,14 @@
#nullable enable
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
@@ -17,6 +19,7 @@ using osu.Framework.Localisation;
using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Configuration;
+using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
@@ -55,18 +58,10 @@ namespace osu.Game.Overlays
///
public FirstRunSetupScreen? CurrentScreen => (FirstRunSetupScreen?)stack?.CurrentScreen;
- private readonly Type[] steps =
- {
- typeof(ScreenWelcome),
- typeof(ScreenBeatmaps),
- typeof(ScreenUIScale),
- typeof(ScreenBehaviour),
- };
+ private readonly List steps = new List();
private Container screenContent = null!;
- private Bindable? overlayActivationMode;
-
private Container content = null!;
private LoadingSpinner loading = null!;
@@ -77,15 +72,22 @@ namespace osu.Game.Overlays
{
}
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ [BackgroundDependencyLoader(permitNulls: true)]
+ private void load(OsuColour colours, LegacyImportManager? legacyImportManager)
{
+ steps.Add(typeof(ScreenWelcome));
+ steps.Add(typeof(ScreenBeatmaps));
+ if (legacyImportManager?.SupportsImportFromStable == true)
+ steps.Add(typeof(ScreenImportFromStable));
+ steps.Add(typeof(ScreenUIScale));
+ steps.Add(typeof(ScreenBehaviour));
+
Header.Title = FirstRunSetupOverlayStrings.FirstRunSetupTitle;
Header.Description = FirstRunSetupOverlayStrings.FirstRunSetupDescription;
MainAreaContent.AddRange(new Drawable[]
{
- content = new Container
+ content = new PopoverContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -221,16 +223,9 @@ namespace osu.Game.Overlays
// if we are valid for display, only do so after reaching the main menu.
performer.PerformFromScreen(screen =>
{
- MainMenu menu = (MainMenu)screen;
-
- // Eventually I'd like to replace this with a better method that doesn't access the screen.
- // Either this dialog would be converted to its own screen, or at very least be "hosted" by a screen pushed to the main menu.
- // Alternatively, another method of disabling notifications could be added to `INotificationOverlay`.
- if (menu != null)
- {
- overlayActivationMode = menu.OverlayActivationMode.GetBoundCopy();
- overlayActivationMode.Value = OverlayActivation.UserTriggered;
- }
+ // Hides the toolbar for us.
+ if (screen is MainMenu menu)
+ menu.ReturnToOsuLogo();
base.Show();
}, new[] { typeof(MainMenu) });
@@ -253,13 +248,6 @@ namespace osu.Game.Overlays
content.ScaleTo(0.99f, 400, Easing.OutQuint);
- if (overlayActivationMode != null)
- {
- // If this is non-null we are guaranteed to have come from the main menu.
- overlayActivationMode.Value = OverlayActivation.All;
- overlayActivationMode = null;
- }
-
if (currentStepIndex != null)
{
notificationOverlay.Post(new SimpleNotification
@@ -313,7 +301,7 @@ namespace osu.Game.Overlays
currentStepIndex++;
- if (currentStepIndex < steps.Length)
+ if (currentStepIndex < steps.Count)
{
var nextScreen = (Screen)Activator.CreateInstance(steps[currentStepIndex.Value]);
@@ -345,7 +333,7 @@ namespace osu.Game.Overlays
return;
bool isFirstStep = currentStepIndex == 0;
- bool isLastStep = currentStepIndex == steps.Length - 1;
+ bool isLastStep = currentStepIndex == steps.Count - 1;
if (isFirstStep)
{
diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
index 1dad185b78..f1a998bd3c 100644
--- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs
+++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
@@ -428,9 +428,8 @@ namespace osu.Game.Overlays.Mods
base.PopIn();
multiplierDisplay?
- .Delay(fade_in_duration * 0.65f)
- .FadeIn(fade_in_duration / 2, Easing.OutQuint)
- .ScaleTo(1, fade_in_duration, Easing.OutElastic);
+ .FadeIn(fade_in_duration, Easing.OutQuint)
+ .MoveToY(0, fade_in_duration, Easing.OutQuint);
int nonFilteredColumnCount = 0;
@@ -465,7 +464,7 @@ namespace osu.Game.Overlays.Mods
multiplierDisplay?
.FadeOut(fade_out_duration / 2, Easing.OutQuint)
- .ScaleTo(0.75f, fade_out_duration, Easing.OutQuint);
+ .MoveToY(-distance, fade_out_duration / 2, Easing.OutQuint);
int nonFilteredColumnCount = 0;
diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
index 216510fcf3..f8d796a778 100644
--- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
@@ -7,6 +7,7 @@ using System.Collections.Specialized;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
@@ -361,20 +362,23 @@ namespace osu.Game.Rulesets.Edit
/// The most relevant .
protected virtual Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => drawableRulesetWrapper.Playfield;
- public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
+ public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
{
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
double? targetTime = null;
- if (playfield is ScrollingPlayfield scrollingPlayfield)
+ if (snapType.HasFlagFast(SnapType.Grids))
{
- targetTime = scrollingPlayfield.TimeAtScreenSpacePosition(screenSpacePosition);
+ if (playfield is ScrollingPlayfield scrollingPlayfield)
+ {
+ targetTime = scrollingPlayfield.TimeAtScreenSpacePosition(screenSpacePosition);
- // apply beat snapping
- targetTime = BeatSnapProvider.SnapTime(targetTime.Value);
+ // apply beat snapping
+ targetTime = BeatSnapProvider.SnapTime(targetTime.Value);
- // convert back to screen space
- screenSpacePosition = scrollingPlayfield.ScreenSpacePositionAtTime(targetTime.Value);
+ // convert back to screen space
+ screenSpacePosition = scrollingPlayfield.ScreenSpacePositionAtTime(targetTime.Value);
+ }
}
return new SnapResult(screenSpacePosition, targetTime, playfield);
@@ -414,10 +418,7 @@ namespace osu.Game.Rulesets.Edit
#region IPositionSnapProvider
- public abstract SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition);
-
- public virtual SnapResult FindSnappedPosition(Vector2 screenSpacePosition) =>
- new SnapResult(screenSpacePosition, null);
+ public abstract SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All);
#endregion
}
diff --git a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs
index 837b04424a..a6a6e39e23 100644
--- a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs
+++ b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs
@@ -8,7 +8,6 @@ namespace osu.Game.Rulesets.Edit
{
///
/// A snap provider which given a proposed position for a hit object, potentially offers a more correct position and time value inferred from the context of the beatmap.
- /// Provided values are inferred in an isolated context, without consideration of other nearby hit objects.
///
[Cached]
public interface IPositionSnapProvider
@@ -16,18 +15,9 @@ namespace osu.Game.Rulesets.Edit
///
/// Given a position, find a valid time and position snap.
///
- ///
- /// This call should be equivalent to running with any additional logic that can be performed without the time immutability restriction.
- ///
/// The screen-space position to be snapped.
+ /// The type of snapping to apply.
/// The time and position post-snapping.
- SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition);
-
- ///
- /// Given a position, find a valid position snap, without changing the time value.
- ///
- /// The screen-space position to be snapped.
- /// The position post-snapping. Time will always be null.
- SnapResult FindSnappedPosition(Vector2 screenSpacePosition);
+ SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All);
}
}
diff --git a/osu.Game/Rulesets/Edit/SnapType.cs b/osu.Game/Rulesets/Edit/SnapType.cs
new file mode 100644
index 0000000000..6761356331
--- /dev/null
+++ b/osu.Game/Rulesets/Edit/SnapType.cs
@@ -0,0 +1,16 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+
+namespace osu.Game.Rulesets.Edit
+{
+ [Flags]
+ public enum SnapType
+ {
+ None = 0,
+ NearbyObjects = 1 << 0,
+ Grids = 1 << 1,
+ All = NearbyObjects | Grids,
+ }
+}
diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
index 2a7f2b037f..7cf68a2df7 100644
--- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
@@ -336,10 +336,14 @@ namespace osu.Game.Rulesets.Objects.Legacy
while (++endIndex < vertices.Length - endPointLength)
{
- // Keep incrementing while an implicit segment doesn't need to be started
+ // Keep incrementing while an implicit segment doesn't need to be started.
if (vertices[endIndex].Position != vertices[endIndex - 1].Position)
continue;
+ // Adjacent legacy Catmull segments should be treated as a single segment.
+ if (FormatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION && type == PathType.Catmull)
+ continue;
+
// The last control point of each segment is not allowed to start a new implicit segment.
if (endIndex == vertices.Length - endPointLength - 1)
continue;
diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs
index 2b75f93f9e..782255733f 100644
--- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
public new ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)base.HitObjectContainer;
[Resolved]
- protected IScrollingInfo ScrollingInfo { get; private set; }
+ public IScrollingInfo ScrollingInfo { get; private set; }
[BackgroundDependencyLoader]
private void load()
diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
index 186c66e0af..d56dc176f6 100644
--- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
@@ -486,7 +486,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
Vector2 originalPosition = movementBlueprintOriginalPositions[i];
var testPosition = originalPosition + distanceTravelled;
- var positionalResult = snapProvider.FindSnappedPosition(testPosition);
+ var positionalResult = snapProvider.FindSnappedPositionAndTime(testPosition, SnapType.NearbyObjects);
if (positionalResult.ScreenSpacePosition == testPosition) continue;
@@ -505,7 +505,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
Vector2 movePosition = movementBlueprintOriginalPositions.First() + distanceTravelled;
// Retrieve a snapped position.
- var result = snapProvider?.FindSnappedPositionAndTime(movePosition);
+ var result = snapProvider?.FindSnappedPositionAndTime(movePosition, ~SnapType.NearbyObjects);
if (result == null)
{
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs
index ab21a83c43..9abea73f6b 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs
@@ -154,7 +154,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void updateBankPlaceholderText(IEnumerable objects)
{
string? commonBank = getCommonBank(objects.Select(h => h.SampleControlPoint).ToArray());
- bank.PlaceholderText = string.IsNullOrEmpty(commonBank) ? "(multiple)" : null;
+ bank.PlaceholderText = string.IsNullOrEmpty(commonBank) ? "(multiple)" : string.Empty;
}
private void updateVolumeFor(IEnumerable objects, int? newVolume)
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
index 7e66c57917..992ab7947e 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
@@ -303,10 +303,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
///
public double VisibleRange => track.Length / Zoom;
- public SnapResult FindSnappedPosition(Vector2 screenSpacePosition) =>
- new SnapResult(screenSpacePosition, null);
-
- public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) =>
+ public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) =>
new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition))));
private double getTimeFromPosition(Vector2 localPosition) =>
diff --git a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs
index d1e35ae20d..aae19396db 100644
--- a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs
+++ b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs
@@ -11,11 +11,8 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface;
-using osu.Framework.Input.Events;
using osu.Game.Database;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osuTK;
@@ -24,7 +21,7 @@ namespace osu.Game.Screens.Edit.Setup
///
/// A labelled textbox which reveals an inline file chooser when clicked.
///
- internal class FileChooserLabelledTextBox : LabelledTextBox, ICanAcceptFiles, IHasPopover
+ internal class FileChooserLabelledTextBox : LabelledTextBoxWithPopover, ICanAcceptFiles
{
private readonly string[] handledExtensions;
@@ -40,16 +37,6 @@ namespace osu.Game.Screens.Edit.Setup
this.handledExtensions = handledExtensions;
}
- protected override OsuTextBox CreateTextBox() =>
- new FileChooserOsuTextBox
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.X,
- CornerRadius = CORNER_RADIUS,
- OnFocused = this.ShowPopover
- };
-
protected override void LoadComplete()
{
base.LoadComplete();
@@ -81,27 +68,7 @@ namespace osu.Game.Screens.Edit.Setup
game.UnregisterImportHandler(this);
}
- internal class FileChooserOsuTextBox : OsuTextBox
- {
- public Action OnFocused;
-
- protected override bool OnDragStart(DragStartEvent e)
- {
- // This text box is intended to be "read only" without actually specifying that.
- // As such we don't want to allow the user to select its content with a drag.
- return false;
- }
-
- protected override void OnFocus(FocusEvent e)
- {
- OnFocused?.Invoke();
- base.OnFocus(e);
-
- GetContainingInputManager().TriggerFocusContention(this);
- }
- }
-
- public Popover GetPopover() => new FileChooserPopover(handledExtensions, currentFile);
+ public override Popover GetPopover() => new FileChooserPopover(handledExtensions, currentFile);
private class FileChooserPopover : OsuPopover
{
diff --git a/osu.Game/Screens/Edit/Setup/LabelledTextBoxWithPopover.cs b/osu.Game/Screens/Edit/Setup/LabelledTextBoxWithPopover.cs
new file mode 100644
index 0000000000..799311dd2d
--- /dev/null
+++ b/osu.Game/Screens/Edit/Setup/LabelledTextBoxWithPopover.cs
@@ -0,0 +1,52 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Input.Events;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Graphics.UserInterfaceV2;
+
+namespace osu.Game.Screens.Edit.Setup
+{
+ internal abstract class LabelledTextBoxWithPopover : LabelledTextBox, IHasPopover
+ {
+ public abstract Popover GetPopover();
+
+ protected override OsuTextBox CreateTextBox() =>
+ new PopoverTextBox
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.X,
+ CornerRadius = CORNER_RADIUS,
+ OnFocused = this.ShowPopover
+ };
+
+ internal class PopoverTextBox : OsuTextBox
+ {
+ public Action OnFocused;
+
+ protected override bool OnDragStart(DragStartEvent e)
+ {
+ // This text box is intended to be "read only" without actually specifying that.
+ // As such we don't want to allow the user to select its content with a drag.
+ return false;
+ }
+
+ protected override void OnFocus(FocusEvent e)
+ {
+ if (Current.Disabled)
+ return;
+
+ OnFocused?.Invoke();
+ base.OnFocus(e);
+
+ GetContainingInputManager().TriggerFocusContention(this);
+ }
+ }
+ }
+}
diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs
index 4401ee93ec..6fc8039413 100644
--- a/osu.Game/Screens/Menu/MainMenu.cs
+++ b/osu.Game/Screens/Menu/MainMenu.cs
@@ -150,6 +150,8 @@ namespace osu.Game.Screens.Menu
[Resolved(canBeNull: true)]
private IPerformFromScreenRunner performer { get; set; }
+ public void ReturnToOsuLogo() => Buttons.State = ButtonSystemState.Initial;
+
private void confirmAndExit()
{
if (exitConfirmed) return;
diff --git a/osu.Game/Utils/SentryLogger.cs b/osu.Game/Utils/SentryLogger.cs
index 5390c666ed..137bf7e0aa 100644
--- a/osu.Game/Utils/SentryLogger.cs
+++ b/osu.Game/Utils/SentryLogger.cs
@@ -8,6 +8,7 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
+using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Logging;
@@ -160,6 +161,8 @@ namespace osu.Game.Utils
};
scope.SetTag(@"ruleset", ruleset.ShortName);
+ scope.SetTag(@"os", $"{RuntimeInfo.OS} ({Environment.OSVersion})");
+ scope.SetTag(@"processor count", Environment.ProcessorCount.ToString());
});
}
else
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 53a4753223..17bc056b59 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -34,7 +34,7 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 2e861faa1f..191d1bd5ca 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -89,6 +89,6 @@
-
+
diff --git a/osu.iOS/Info.plist b/osu.iOS/Info.plist
index 02968b87a7..16cb68fa7d 100644
--- a/osu.iOS/Info.plist
+++ b/osu.iOS/Info.plist
@@ -33,6 +33,8 @@
UIStatusBarHidden
+ UIApplicationSupportsIndirectInputEvents
+
CADisableMinimumFrameDurationOnPhone
NSCameraUsageDescription