Merge branch 'master' into new-chat-remove-selector-item

This commit is contained in:
Jai Sharma 2022-05-21 17:30:06 +01:00
commit fb06b7658b
103 changed files with 1298 additions and 590 deletions

View File

@ -31,7 +31,7 @@ jobs:
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
path: ${{ github.workspace }}/inspectcode 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 - name: Dotnet code style
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf -p:EnforceCodeStyleInBuild=true run: dotnet build -c Debug -warnaserror osu.Desktop.slnf -p:EnforceCodeStyleInBuild=true

26
.github/workflows/sentry-release.yml vendored Normal file
View File

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

2
.gitignore vendored
View File

@ -340,3 +340,5 @@ inspectcode
# Fody (pulled in by Realm) - schema file # Fody (pulled in by Realm) - schema file
FodyWeavers.xsd FodyWeavers.xsd
**/FodyWeavers.xml **/FodyWeavers.xml
.idea/.idea.osu.Desktop/.idea/misc.xml

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SwUserDefinedSpecifications">
<option name="specTypeByUrl">
<map />
</option>
</component>
<component name="com.jetbrains.rider.android.RiderAndroidMiscFileCreationComponent">
<option name="ENSURE_MISC_FILE_EXISTS" value="true" />
</component>
</project>

View File

@ -11,6 +11,7 @@ T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal exten
T:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods. T:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods.
M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast<T>() instead. M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast<T>() instead.
M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection<T>,NotificationCallbackDelegate<T>) instead. M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection<T>,NotificationCallbackDelegate<T>) 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<T>,NotificationCallbackDelegate<T>) instead. M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IQueryable<T>,NotificationCallbackDelegate<T>) instead.
M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IList<T>,NotificationCallbackDelegate<T>) instead. M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IList<T>,NotificationCallbackDelegate<T>) instead.
M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks. M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks.

View File

@ -52,10 +52,10 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.513.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.513.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.511.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2022.521.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. --> <!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
<PackageReference Include="Realm" Version="10.12.0" /> <PackageReference Include="Realm" Version="10.11.2" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -18,5 +18,6 @@
<file src="**.exe" target="lib\net45\" exclude="**vshost**"/> <file src="**.exe" target="lib\net45\" exclude="**vshost**"/>
<file src="**.dll" target="lib\net45\"/> <file src="**.dll" target="lib\net45\"/>
<file src="**.config" target="lib\net45\"/> <file src="**.config" target="lib\net45\"/>
<file src="**.json" target="lib\net45\"/>
</files> </files>
</package> </package>

View File

@ -37,6 +37,8 @@
</array> </array>
<key>XSAppIconAssets</key> <key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string> <string>Assets.xcassets/AppIcon.appiconset</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
</dict> </dict>

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
{ {
public class TestSceneJuiceStreamPlacementBlueprint : CatchPlacementBlueprintTestScene public class TestSceneJuiceStreamPlacementBlueprint : CatchPlacementBlueprintTestScene
{ {
private const double velocity = 0.5; private const double velocity_factor = 0.5;
private JuiceStream lastObject => LastObject?.HitObject as JuiceStream; private JuiceStream lastObject => LastObject?.HitObject as JuiceStream;
@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
{ {
var playable = base.GetPlayableBeatmap(); var playable = base.GetPlayableBeatmap();
playable.Difficulty.SliderTickRate = 5; playable.Difficulty.SliderTickRate = 5;
playable.Difficulty.SliderMultiplier = velocity * 10; playable.Difficulty.SliderMultiplier = velocity_factor * 10;
return playable; 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("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1]));
AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0])); AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0]));
AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1])); AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1]));
AddAssert("default slider velocity", () => lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
} }
[Test] [Test]
@ -66,28 +67,21 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
} }
[Test] [Test]
public void TestVelocityLimit() public void TestSliderVelocityChange()
{ {
double[] times = { 100, 300 }; double[] times = { 100, 300 };
float[] positions = { 200, 500 }; float[] positions = { 200, 500 };
addPlacementSteps(times, positions); addPlacementSteps(times, positions);
addPathCheckStep(times, new float[] { 200, 300 }); addPathCheckStep(times, positions);
}
[Test] AddAssert("slider velocity changed", () => !lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
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 });
} }
[Test] [Test]
public void TestClampedPositionIsRestored() public void TestClampedPositionIsRestored()
{ {
double[] times = { 100, 300, 500 }; double[] times = { 100, 300, 500 };
float[] positions = { 200, 200, 0, 250 }; float[] positions = { 200, 200, -3000, 250 };
addMoveAndClickSteps(times[0], positions[0]); addMoveAndClickSteps(times[0], positions[0]);
addMoveAndClickSteps(times[1], positions[1]); addMoveAndClickSteps(times[1], positions[1]);
@ -97,15 +91,6 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
addPathCheckStep(times, new float[] { 200, 200, 250 }); 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] [Test]
public void TestOutOfOrder() public void TestOutOfOrder()
{ {

View File

@ -101,31 +101,16 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
} }
[Test] [Test]
public void TestClampedPositionIsRestored() public void TestSliderVelocityChange()
{ {
const double velocity = 0.25; double[] times = { 100, 300 };
double[] times = { 100, 500, 700 }; float[] positions = { 200, 300 };
float[] positions = { 100, 100, 100 }; addBlueprintStep(times, positions);
addBlueprintStep(times, positions, velocity); AddAssert("default slider velocity", () => hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
addDragStartStep(times[1], positions[1]); addDragStartStep(times[1], positions[1]);
AddMouseMoveStep(times[1], 400);
AddMouseMoveStep(times[1], 200); AddAssert("slider velocity changed", () => !hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
addVertexCheckStep(3, 1, times[1], 200);
addVertexCheckStep(3, 2, times[2], 150);
AddMouseMoveStep(times[1], 100);
addVertexCheckStep(3, 1, times[1], 100);
// Stored position is restored.
addVertexCheckStep(3, 2, times[2], positions[2]);
AddMouseMoveStep(times[1], 300);
addDragEndStep();
addDragStartStep(times[1], 300);
AddMouseMoveStep(times[1], 100);
// Position is different because a changed position is committed when the previous drag is ended.
addVertexCheckStep(3, 2, times[2], 250);
} }
[Test] [Test]
@ -174,7 +159,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
addAddVertexSteps(500, 150); addAddVertexSteps(500, 150);
addVertexCheckStep(3, 1, 500, 150); addVertexCheckStep(3, 1, 500, 150);
addAddVertexSteps(90, 220); addAddVertexSteps(90, 200);
addVertexCheckStep(4, 1, times[0], positions[0]); addVertexCheckStep(4, 1, times[0], positions[0]);
addAddVertexSteps(750, 180); addAddVertexSteps(750, 180);
@ -234,10 +219,10 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
{ {
var path = new JuiceStreamPath(); var path = new JuiceStreamPath();
for (int i = 1; i < times.Length; i++) 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(); var sliderPath = new SliderPath();
path.ConvertToSliderPath(sliderPath, 0); path.ConvertToSliderPath(sliderPath, 0, velocity);
addBlueprintStep(times[0], positions[0], sliderPath, 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}", () => 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; float expectedX = x - hitObject.OriginalX;
var vertices = getVertices(); var vertices = getVertices();
return vertices.Count == count && 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); Precision.AlmostEquals(vertices[index].X, expectedX);
}); });

View File

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
@ -37,14 +36,14 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
case 0: case 0:
{ {
double distance = rng.NextDouble() * scale * 2 - scale; double time = rng.NextDouble() * scale * 2 - scale;
if (integralValues) if (integralValues)
distance = Math.Round(distance); time = Math.Round(time);
float oldX = path.PositionAtDistance(distance); float oldX = path.PositionAtTime(time);
int index = path.InsertVertex(distance); int index = path.InsertVertex(time);
Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount + 1)); 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)); Assert.That(path.Vertices[index].X, Is.EqualTo(oldX));
break; break;
} }
@ -52,20 +51,20 @@ namespace osu.Game.Rulesets.Catch.Tests
case 1: case 1:
{ {
int index = rng.Next(path.Vertices.Count); 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); float newX = (float)(rng.NextDouble() * scale * 2 - scale);
if (integralValues) if (integralValues)
newX = MathF.Round(newX); newX = MathF.Round(newX);
path.SetVertexPosition(index, newX); path.SetVertexPosition(index, newX);
Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount)); 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)); Assert.That(path.Vertices[index].X, Is.EqualTo(newX));
break; break;
} }
} }
assertInvariants(path.Vertices, checkSlope); assertInvariants(path.Vertices);
} }
} }
@ -76,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Tests
path.Add(10, 5); path.Add(10, 5);
path.Add(20, -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(removeCount, Is.EqualTo(1));
Assert.That(path.Vertices, Is.EqualTo(new[] Assert.That(path.Vertices, Is.EqualTo(new[]
{ {
@ -131,8 +130,9 @@ namespace osu.Game.Rulesets.Catch.Tests
})); }));
} }
[Test] [TestCase(10)]
public void TestRandomConvertFromSliderPath() [TestCase(0.1)]
public void TestRandomConvertFromSliderPath(double velocity)
{ {
var rng = new Random(1); var rng = new Random(1);
var path = new JuiceStreamPath(); var path = new JuiceStreamPath();
@ -162,28 +162,28 @@ namespace osu.Game.Rulesets.Catch.Tests
else else
sliderPath.ExpectedDistance.Value = null; sliderPath.ExpectedDistance.Value = null;
path.ConvertFromSliderPath(sliderPath); path.ConvertFromSliderPath(sliderPath, velocity);
Assert.That(path.Vertices[0].Distance, Is.EqualTo(0)); Assert.That(path.Vertices[0].Time, Is.EqualTo(0));
Assert.That(path.Distance, Is.EqualTo(sliderPath.Distance).Within(1e-3)); Assert.That(path.Duration * velocity, Is.EqualTo(sliderPath.Distance).Within(1e-3));
assertInvariants(path.Vertices, true); assertInvariants(path.Vertices);
double[] sampleDistances = Enumerable.Range(0, 10) double[] sampleTimes = Enumerable.Range(0, 10)
.Select(_ => rng.NextDouble() * sliderPath.Distance) .Select(_ => rng.NextDouble() * sliderPath.Distance / velocity)
.ToArray(); .ToArray();
foreach (double distance in sampleDistances) foreach (double time in sampleTimes)
{ {
float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X; float expected = sliderPath.PositionAt(time * velocity / sliderPath.Distance).X;
Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3)); Assert.That(path.PositionAtTime(time), Is.EqualTo(expected).Within(1e-3));
} }
path.ResampleVertices(sampleDistances); path.ResampleVertices(sampleTimes);
assertInvariants(path.Vertices, true); assertInvariants(path.Vertices);
foreach (double distance in sampleDistances) foreach (double time in sampleTimes)
{ {
float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X; float expected = sliderPath.PositionAt(time * velocity / sliderPath.Distance).X;
Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3)); Assert.That(path.PositionAtTime(time), Is.EqualTo(expected).Within(1e-3));
} }
} }
} }
@ -201,17 +201,17 @@ namespace osu.Game.Rulesets.Catch.Tests
do do
{ {
double distance = rng.NextDouble() * 1e3; double time = rng.NextDouble() * 1e3;
float x = (float)(rng.NextDouble() * 1e3); float x = (float)(rng.NextDouble() * 1e3);
path.Add(distance, x); path.Add(time, x);
} while (rng.Next(5) != 0); } while (rng.Next(5) != 0);
float sliderStartY = (float)(rng.NextDouble() * JuiceStreamPath.OSU_PLAYFIELD_HEIGHT); float sliderStartY = (float)(rng.NextDouble() * JuiceStreamPath.OSU_PLAYFIELD_HEIGHT);
path.ConvertToSliderPath(sliderPath, sliderStartY); double requiredVelocity = path.ComputeRequiredVelocity();
Assert.That(sliderPath.Distance, Is.EqualTo(path.Distance).Within(1e-3)); double velocity = Math.Clamp(requiredVelocity, 1, 100);
Assert.That(sliderPath.ControlPoints[0].Position.X, Is.EqualTo(path.Vertices[0].X));
assertInvariants(path.Vertices, true); path.ConvertToSliderPath(sliderPath, sliderStartY, velocity);
foreach (var point in sliderPath.ControlPoints) 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(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++) for (int i = 0; i < 10; i++)
{ {
double distance = rng.NextDouble() * path.Distance; double time = rng.NextDouble() * path.Duration;
float expected = path.PositionAtDistance(distance); float expected = path.PositionAtTime(time);
Assert.That(sliderPath.PositionAt(distance / sliderPath.Distance).X, Is.EqualTo(expected).Within(1e-3)); 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); path.Add(20, 0);
checkNewId(); checkNewId();
path.RemoveVertices((v, _) => v.Distance == 20); path.RemoveVertices((v, _) => v.Time == 20);
checkNewId(); checkNewId();
path.ResampleVertices(new double[] { 5, 10, 15 }); path.ResampleVertices(new double[] { 5, 10, 15 });
@ -253,7 +260,7 @@ namespace osu.Game.Rulesets.Catch.Tests
path.Clear(); path.Clear();
checkNewId(); checkNewId();
path.ConvertFromSliderPath(new SliderPath()); path.ConvertFromSliderPath(new SliderPath(), 1);
checkNewId(); checkNewId();
void checkNewId() void checkNewId()
@ -263,25 +270,19 @@ namespace osu.Game.Rulesets.Catch.Tests
} }
} }
private void assertInvariants(IReadOnlyList<JuiceStreamPathVertex> vertices, bool checkSlope) private void assertInvariants(IReadOnlyList<JuiceStreamPathVertex> vertices)
{ {
Assert.That(vertices, Is.Not.Empty); Assert.That(vertices, Is.Not.Empty);
for (int i = 0; i < vertices.Count; i++) 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)); Assert.That(float.IsFinite(vertices[i].X));
} }
for (int i = 1; i < vertices.Count; i++) for (int i = 1; i < vertices.Count; i++)
{ {
Assert.That(vertices[i].Distance, Is.GreaterThanOrEqualTo(vertices[i - 1].Distance)); Assert.That(vertices[i].Time, Is.GreaterThanOrEqualTo(vertices[i - 1].Time));
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));
} }
} }
} }

View File

@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
public int VertexCount => path.Vertices.Count; public int VertexCount => path.Vertices.Count;
protected readonly Func<float, double> PositionToDistance; protected readonly Func<float, double> PositionToTime;
protected IReadOnlyList<VertexState> VertexStates => vertexStates; protected IReadOnlyList<VertexState> VertexStates => vertexStates;
@ -44,9 +44,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
[CanBeNull] [CanBeNull]
private IBeatSnapProvider beatSnapProvider { get; set; } private IBeatSnapProvider beatSnapProvider { get; set; }
protected EditablePath(Func<float, double> positionToDistance) protected EditablePath(Func<float, double> positionToTime)
{ {
PositionToDistance = positionToDistance; PositionToTime = positionToTime;
Anchor = Anchor.BottomLeft; Anchor = Anchor.BottomLeft;
} }
@ -59,13 +59,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
while (InternalChildren.Count < path.Vertices.Count) while (InternalChildren.Count < path.Vertices.Count)
AddInternal(new VertexPiece()); 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++) for (int i = 0; i < VertexCount; i++)
{ {
var piece = (VertexPiece)InternalChildren[i]; var piece = (VertexPiece)InternalChildren[i];
var vertex = path.Vertices[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]); piece.UpdateFrom(vertexStates[i]);
} }
} }
@ -73,14 +73,14 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
public void InitializeFromHitObject(JuiceStream hitObject) public void InitializeFromHitObject(JuiceStream hitObject)
{ {
var sliderPath = hitObject.Path; 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 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)) if (sliderPath.ControlPoints.Any(p => p.Type != null && p.Type != PathType.Linear))
{ {
path.ResampleVertices(hitObject.NestedHitObjects path.ResampleVertices(hitObject.NestedHitObjects
.Skip(1).TakeWhile(h => !(h is Fruit)) // Only droplets in the first span are used. .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(); vertexStates.Clear();
@ -92,11 +92,26 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
public void UpdateHitObjectFromPath(JuiceStream hitObject) 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; 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); double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime);
hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity; 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 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); path.SetVertexPosition(index, x);
vertexStates.Insert(index, new VertexState()); vertexStates.Insert(index, new VertexState());
@ -138,9 +153,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
return true; 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.Clear();
previousVertexStates.AddRange(vertexStates); previousVertexStates.AddRange(vertexStates);
@ -152,11 +167,11 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
for (int i = 1; i < vertexCount; i++) for (int i = 1; i < vertexCount; i++)
{ {
var state = previousVertexStates[i]; var state = previousVertexStates[i];
double distance = state.VertexBeforeChange.Distance; double time = state.VertexBeforeChange.Time;
if (state.IsSelected) 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); vertexStates.Insert(newIndex, state);
} }

View File

@ -15,15 +15,15 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
/// </summary> /// </summary>
private JuiceStreamPathVertex lastVertex; private JuiceStreamPathVertex lastVertex;
public PlacementEditablePath(Func<float, double> positionToDistance) public PlacementEditablePath(Func<float, double> positionToTime)
: base(positionToDistance) : base(positionToTime)
{ {
} }
public void AddNewVertex() public void AddNewVertex()
{ {
var endVertex = Vertices[^1]; 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++) for (int i = 0; i < VertexCount; i++)
{ {
@ -41,9 +41,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
public void MoveLastVertex(Vector2 screenSpacePosition) public void MoveLastVertex(Vector2 screenSpacePosition)
{ {
Vector2 position = ToRelativePosition(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; float xDelta = position.X - lastVertex.X;
MoveSelectedVertices(distanceDelta, xDelta); MoveSelectedVertices(timeDelta, xDelta);
} }
} }
} }

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{ {
private readonly Path drawablePath; 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() public ScrollingPath()
{ {
@ -35,16 +35,16 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
public void UpdatePathFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject) 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 drawablePath.Vertices = vertices
.Select(v => new Vector2(v.X, (float)(v.Distance * distanceToYFactor))) .Select(v => new Vector2(v.X, (float)(v.Time * timeToYFactor)))
.ToArray(); .ToArray();
drawablePath.OriginPosition = drawablePath.PositionInBoundingBox(Vector2.Zero); drawablePath.OriginPosition = drawablePath.PositionInBoundingBox(Vector2.Zero);
} }
private void computeDistanceXs(JuiceStream hitObject) private void computeTimeXs(JuiceStream hitObject)
{ {
vertices.Clear(); vertices.Clear();
@ -54,17 +54,17 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
if (sliderVertices.Count == 0) if (sliderVertices.Count == 0)
return; return;
double distance = 0; double time = 0;
Vector2 lastPosition = Vector2.Zero; Vector2 lastPosition = Vector2.Zero;
for (int repeat = 0; repeat < hitObject.RepeatCount + 1; repeat++) for (int repeat = 0; repeat < hitObject.RepeatCount + 1; repeat++)
{ {
foreach (var position in sliderVertices) foreach (var position in sliderVertices)
{ {
distance += Vector2.Distance(lastPosition, position); time += Vector2.Distance(lastPosition, position) / hitObject.Velocity;
lastPosition = position; lastPosition = position;
vertices.Add((distance, position.X)); vertices.Add((time, position.X));
} }
sliderVertices.Reverse(); sliderVertices.Reverse();

View File

@ -27,15 +27,15 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
[CanBeNull] [CanBeNull]
private IEditorChangeHandler changeHandler { get; set; } private IEditorChangeHandler changeHandler { get; set; }
public SelectionEditablePath(Func<float, double> positionToDistance) public SelectionEditablePath(Func<float, double> positionToTime)
: base(positionToDistance) : base(positionToTime)
{ {
} }
public void AddVertex(Vector2 relativePosition) public void AddVertex(Vector2 relativePosition)
{ {
double distance = Math.Max(0, PositionToDistance(relativePosition.Y)); double time = Math.Max(0, PositionToTime(relativePosition.Y));
int index = AddVertex(distance, relativePosition.X); int index = AddVertex(time, relativePosition.X);
selectOnly(index); selectOnly(index);
} }
@ -83,9 +83,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
protected override void OnDrag(DragEvent e) protected override void OnDrag(DragEvent e)
{ {
Vector2 mousePosition = ToRelativePosition(e.ScreenSpaceMousePosition); 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; float xDelta = mousePosition.X - dragStartPosition.X;
MoveSelectedVertices(distanceDelta, xDelta); MoveSelectedVertices(timeDelta, xDelta);
} }
protected override void OnDragEnd(DragEndEvent e) protected override void OnDragEnd(DragEndEvent e)

View File

@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{ {
scrollingPath = new ScrollingPath(), scrollingPath = new ScrollingPath(),
nestedOutlineContainer = new NestedOutlineContainer(), 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; lastEditablePathId = editablePath.PathId;
} }
private double positionToDistance(float relativeYPosition) private double positionToTime(float relativeYPosition)
{ {
double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime); double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime);
return (time - HitObject.StartTime) * HitObject.Velocity; return time - HitObject.StartTime;
} }
} }
} }

View File

@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{ {
scrollingPath = new ScrollingPath(), scrollingPath = new ScrollingPath(),
nestedOutlineContainer = new NestedOutlineContainer(), 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); 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); double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime);
return (time - HitObject.StartTime) * HitObject.Velocity; return time - HitObject.StartTime;
} }
private void initializeJuiceStreamPath() private void initializeJuiceStreamPath()

View File

@ -6,6 +6,7 @@ using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input; 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 }) 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; result.ScreenSpacePosition.X = screenSpacePosition.X;
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult && if (snapType.HasFlagFast(SnapType.Grids))
Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius)
{ {
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; return result;

View File

@ -27,10 +27,16 @@ namespace osu.Game.Rulesets.Catch.Objects
public int RepeatCount { get; set; } public int RepeatCount { get; set; }
[JsonIgnore] [JsonIgnore]
public double Velocity { get; private set; } private double velocityFactor;
[JsonIgnore] [JsonIgnore]
public double TickDistance { get; private set; } private double tickDistanceFactor;
[JsonIgnore]
public double Velocity => velocityFactor * DifficultyControlPoint.SliderVelocity;
[JsonIgnore]
public double TickDistance => tickDistanceFactor * DifficultyControlPoint.SliderVelocity;
/// <summary> /// <summary>
/// The length of one span of this <see cref="JuiceStream"/>. /// The length of one span of this <see cref="JuiceStream"/>.
@ -43,10 +49,8 @@ namespace osu.Game.Rulesets.Catch.Objects
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity; velocityFactor = base_scoring_distance * difficulty.SliderMultiplier / timingPoint.BeatLength;
tickDistanceFactor = base_scoring_distance * difficulty.SliderMultiplier / difficulty.SliderTickRate;
Velocity = scoringDistance / timingPoint.BeatLength;
TickDistance = scoringDistance / difficulty.SliderTickRate;
} }
protected override void CreateNestedHitObjects(CancellationToken cancellationToken) protected override void CreateNestedHitObjects(CancellationToken cancellationToken)

View File

@ -20,11 +20,6 @@ namespace osu.Game.Rulesets.Catch.Objects
/// However, the <see cref="SliderPath"/> representation is difficult to work with. /// However, the <see cref="SliderPath"/> representation is difficult to work with.
/// This <see cref="JuiceStreamPath"/> represents the path in a more convenient way, a polyline connecting list of <see cref="JuiceStreamPathVertex"/>s. /// This <see cref="JuiceStreamPath"/> represents the path in a more convenient way, a polyline connecting list of <see cref="JuiceStreamPathVertex"/>s.
/// </para> /// </para>
/// <para>
/// The path can be regarded as a function from the closed interval <c>[Vertices[0].Distance, Vertices[^1].Distance]</c> to the x position, given by <see cref="PositionAtDistance"/>.
/// To ensure the path is convertible to a <see cref="SliderPath"/>, the slope of the function must not be more than <c>1</c> everywhere,
/// and this slope condition is always maintained as an invariant.
/// </para>
/// </summary> /// </summary>
public class JuiceStreamPath public class JuiceStreamPath
{ {
@ -46,9 +41,9 @@ namespace osu.Game.Rulesets.Catch.Objects
public int InvalidationID { get; private set; } = 1; public int InvalidationID { get; private set; } = 1;
/// <summary> /// <summary>
/// The difference between first vertex's <see cref="JuiceStreamPathVertex.Distance"/> and last vertex's <see cref="JuiceStreamPathVertex.Distance"/>. /// The difference between first vertex's <see cref="JuiceStreamPathVertex.Time"/> and last vertex's <see cref="JuiceStreamPathVertex.Time"/>.
/// </summary> /// </summary>
public double Distance => vertices[^1].Distance - vertices[0].Distance; public double Duration => vertices[^1].Time - vertices[0].Time;
/// <remarks> /// <remarks>
/// This list should always be non-empty. /// This list should always be non-empty.
@ -59,15 +54,15 @@ namespace osu.Game.Rulesets.Catch.Objects
}; };
/// <summary> /// <summary>
/// Compute the x-position of the path at the given <paramref name="distance"/>. /// Compute the x-position of the path at the given <paramref name="time"/>.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// 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,
/// </remarks> /// </remarks>
public float PositionAtDistance(double distance) public float PositionAtTime(double time)
{ {
int index = vertexIndexAtDistance(distance); int index = vertexIndexAtTime(time);
return positionAtDistance(distance, index); return positionAtTime(time, index);
} }
/// <summary> /// <summary>
@ -81,19 +76,19 @@ namespace osu.Game.Rulesets.Catch.Objects
} }
/// <summary> /// <summary>
/// Insert a vertex at given <paramref name="distance"/>. /// Insert a vertex at given <paramref name="time"/>.
/// The <see cref="PositionAtDistance"/> is used as the position of the new vertex. /// The <see cref="PositionAtTime"/> 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). /// Thus, the set of points of the path is not changed (up to floating-point precision).
/// </summary> /// </summary>
/// <returns>The index of the new vertex.</returns> /// <returns>The index of the new vertex.</returns>
public int InsertVertex(double distance) public int InsertVertex(double time)
{ {
if (!double.IsFinite(distance)) if (!double.IsFinite(time))
throw new ArgumentOutOfRangeException(nameof(distance)); throw new ArgumentOutOfRangeException(nameof(time));
int index = vertexIndexAtDistance(distance); int index = vertexIndexAtTime(time);
float x = positionAtDistance(distance, index); float x = positionAtTime(time, index);
vertices.Insert(index, new JuiceStreamPathVertex(distance, x)); vertices.Insert(index, new JuiceStreamPathVertex(time, x));
invalidate(); invalidate();
return index; return index;
@ -101,7 +96,6 @@ namespace osu.Game.Rulesets.Catch.Objects
/// <summary> /// <summary>
/// Move the vertex of given <paramref name="index"/> to the given position <paramref name="newX"/>. /// Move the vertex of given <paramref name="index"/> to the given position <paramref name="newX"/>.
/// When the distances between vertices are too small for the new vertex positions, the adjacent vertices are moved towards <paramref name="newX"/>.
/// </summary> /// </summary>
public void SetVertexPosition(int index, float newX) public void SetVertexPosition(int index, float newX)
{ {
@ -111,32 +105,17 @@ namespace osu.Game.Rulesets.Catch.Objects
if (!float.IsFinite(newX)) if (!float.IsFinite(newX))
throw new ArgumentOutOfRangeException(nameof(newX)); throw new ArgumentOutOfRangeException(nameof(newX));
var newVertex = new JuiceStreamPathVertex(vertices[index].Distance, newX); vertices[index] = new JuiceStreamPathVertex(vertices[index].Time, 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;
invalidate(); invalidate();
} }
/// <summary> /// <summary>
/// Add a new vertex at given <paramref name="distance"/> and position. /// Add a new vertex at given <paramref name="time"/> and position.
/// Adjacent vertices are moved when necessary in the same way as <see cref="SetVertexPosition"/>.
/// </summary> /// </summary>
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); SetVertexPosition(index, x);
} }
@ -163,22 +142,22 @@ namespace osu.Game.Rulesets.Catch.Objects
} }
/// <summary> /// <summary>
/// Recreate this path by using difference set of vertices at given distances. /// Recreate this path by using difference set of vertices at given time points.
/// In addition to the given <paramref name="sampleDistances"/>, the first vertex and the last vertex are always added to the new path. /// In addition to the given <paramref name="sampleTimes"/>, the first vertex and the last vertex are always added to the new path.
/// New vertices use the positions on the original path. Thus, <see cref="PositionAtDistance"/>s at <paramref name="sampleDistances"/> are preserved. /// New vertices use the positions on the original path. Thus, <see cref="PositionAtTime"/>s at <paramref name="sampleTimes"/> are preserved.
/// </summary> /// </summary>
public void ResampleVertices(IEnumerable<double> sampleDistances) public void ResampleVertices(IEnumerable<double> sampleTimes)
{ {
var sampledVertices = new List<JuiceStreamPathVertex>(); var sampledVertices = new List<JuiceStreamPathVertex>();
foreach (double distance in sampleDistances) foreach (double time in sampleTimes)
{ {
if (!double.IsFinite(distance)) if (!double.IsFinite(time))
throw new ArgumentOutOfRangeException(nameof(sampleDistances)); throw new ArgumentOutOfRangeException(nameof(sampleTimes));
double clampedDistance = Math.Clamp(distance, vertices[0].Distance, vertices[^1].Distance); double clampedTime = Math.Clamp(time, vertices[0].Time, vertices[^1].Time);
float x = PositionAtDistance(clampedDistance); float x = PositionAtTime(clampedTime);
sampledVertices.Add(new JuiceStreamPathVertex(clampedDistance, x)); sampledVertices.Add(new JuiceStreamPathVertex(clampedTime, x));
} }
sampledVertices.Sort(); sampledVertices.Sort();
@ -196,37 +175,62 @@ namespace osu.Game.Rulesets.Catch.Objects
/// <remarks> /// <remarks>
/// Duplicated vertices are automatically removed. /// Duplicated vertices are automatically removed.
/// </remarks> /// </remarks>
public void ConvertFromSliderPath(SliderPath sliderPath) public void ConvertFromSliderPath(SliderPath sliderPath, double velocity)
{ {
var sliderPathVertices = new List<Vector2>(); var sliderPathVertices = new List<Vector2>();
sliderPath.GetPathToProgress(sliderPathVertices, 0, 1); sliderPath.GetPathToProgress(sliderPathVertices, 0, 1);
double distance = 0; double time = 0;
vertices.Clear(); vertices.Clear();
vertices.Add(new JuiceStreamPathVertex(0, sliderPathVertices.FirstOrDefault().X)); vertices.Add(new JuiceStreamPathVertex(0, sliderPathVertices.FirstOrDefault().X));
for (int i = 1; i < sliderPathVertices.Count; i++) 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)) if (!Precision.AlmostEquals(vertices[^1].Time, time))
vertices.Add(new JuiceStreamPathVertex(distance, sliderPathVertices[i].X)); Add(time, sliderPathVertices[i].X);
} }
invalidate(); invalidate();
} }
/// <summary>
/// Computes the minimum slider velocity required to convert this path to a <see cref="SliderPath"/>.
/// </summary>
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;
}
/// <summary> /// <summary>
/// Convert the path of this <see cref="JuiceStreamPath"/> to a <see cref="SliderPath"/> and write the result to <paramref name="sliderPath"/>. /// Convert the path of this <see cref="JuiceStreamPath"/> to a <see cref="SliderPath"/> and write the result to <paramref name="sliderPath"/>.
/// The resulting slider is "folded" to make it vertically contained in the playfield `(0..<see cref="OSU_PLAYFIELD_HEIGHT"/>)` assuming the slider start position is <paramref name="sliderStartY"/>. /// The resulting slider is "folded" to make it vertically contained in the playfield `(0..<see cref="OSU_PLAYFIELD_HEIGHT"/>)` assuming the slider start position is <paramref name="sliderStartY"/>.
///
/// The velocity of the converted slider is assumed to be <paramref name="velocity"/>.
/// To preserve the path, <paramref name="velocity"/> should be at least the value returned by <see cref="ComputeRequiredVelocity"/>.
/// </summary> /// </summary>
public void ConvertToSliderPath(SliderPath sliderPath, float sliderStartY) public void ConvertToSliderPath(SliderPath sliderPath, float sliderStartY, double velocity)
{ {
const float margin = 1; const float margin = 1;
// Note: these two variables and `sliderPath` are modified by the local functions. // 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); Vector2 lastPosition = new Vector2(vertices[0].X, 0);
sliderPath.ControlPoints.Clear(); sliderPath.ControlPoints.Clear();
@ -237,10 +241,10 @@ namespace osu.Game.Rulesets.Catch.Objects
sliderPath.ControlPoints[^1].Type = PathType.Linear; sliderPath.ControlPoints[^1].Type = PathType.Linear;
float deltaX = vertices[i].X - lastPosition.X; 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`. // 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)); double deltaY = Math.Sqrt(Math.Max(0, length * length - (double)deltaX * deltaX));
// When `deltaY` is small, one segment is always enough. // 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); Vector2 nextPosition = new Vector2(nextX, nextY);
sliderPath.ControlPoints.Add(new PathControlPoint(nextPosition)); sliderPath.ControlPoints.Add(new PathControlPoint(nextPosition));
currentDistance += Vector2.Distance(lastPosition, nextPosition); currentTime += Vector2.Distance(lastPosition, nextPosition) / velocity;
lastPosition = nextPosition; lastPosition = nextPosition;
} }
} }
/// <summary> /// <summary>
/// Find the index at which a new vertex with <paramref name="distance"/> can be inserted. /// Find the index at which a new vertex with <paramref name="time"/> can be inserted.
/// </summary> /// </summary>
private int vertexIndexAtDistance(double distance) private int vertexIndexAtTime(double time)
{ {
// The position of `(distance, Infinity)` is uniquely determined because infinite positions are not allowed. // The position of `(time, Infinity)` is uniquely determined because infinite positions are not allowed.
int i = vertices.BinarySearch(new JuiceStreamPathVertex(distance, float.PositiveInfinity)); int i = vertices.BinarySearch(new JuiceStreamPathVertex(time, float.PositiveInfinity));
return i < 0 ? ~i : i; return i < 0 ? ~i : i;
} }
/// <summary> /// <summary>
/// Compute the position at the given <paramref name="distance"/>, assuming <paramref name="index"/> is the vertex index returned by <see cref="vertexIndexAtDistance"/>. /// Compute the position at the given <paramref name="time"/>, assuming <paramref name="index"/> is the vertex index returned by <see cref="vertexIndexAtTime"/>.
/// </summary> /// </summary>
private float positionAtDistance(double distance, int index) private float positionAtTime(double time, int index)
{ {
if (index <= 0) if (index <= 0)
return vertices[0].X; return vertices[0].X;
if (index >= vertices.Count) if (index >= vertices.Count)
return vertices[^1].X; return vertices[^1].X;
double length = vertices[index].Distance - vertices[index - 1].Distance; double duration = vertices[index].Time - vertices[index - 1].Time;
if (Precision.AlmostEquals(length, 0)) if (Precision.AlmostEquals(duration, 0))
return vertices[index].X; return vertices[index].X;
float deltaX = vertices[index].X - vertices[index - 1].X; float deltaX = vertices[index].X - vertices[index - 1].X;
return (float)(vertices[index - 1].X + deltaX * ((distance - vertices[index - 1].Distance) / length)); return (float)(vertices[index - 1].X + deltaX * ((time - vertices[index - 1].Time) / duration));
}
/// <summary>
/// Check the two vertices can connected directly while satisfying the slope condition.
/// </summary>
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;
}
/// <summary>
/// Move the position of <paramref name="movableVertex"/> towards the position of <paramref name="fixedVertex"/>
/// until the vertex pair satisfies the condition <see cref="canConnect"/>.
/// </summary>
/// <returns>The resulting position of <paramref name="movableVertex"/>.</returns>
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);
} }
private void invalidate() => InvalidationID++; private void invalidate() => InvalidationID++;

View File

@ -12,22 +12,22 @@ namespace osu.Game.Rulesets.Catch.Objects
/// </summary> /// </summary>
public readonly struct JuiceStreamPathVertex : IComparable<JuiceStreamPathVertex> public readonly struct JuiceStreamPathVertex : IComparable<JuiceStreamPathVertex>
{ {
public readonly double Distance; public readonly double Time;
public readonly float X; public readonly float X;
public JuiceStreamPathVertex(double distance, float x) public JuiceStreamPathVertex(double time, float x)
{ {
Distance = distance; Time = time;
X = x; X = x;
} }
public int CompareTo(JuiceStreamPathVertex other) 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); return c != 0 ? c : X.CompareTo(other.X);
} }
public override string ToString() => $"({Distance}, {X})"; public override string ToString() => $"({Time}, {X})";
} }
} }

View File

@ -37,6 +37,8 @@
</array> </array>
<key>XSAppIconAssets</key> <key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string> <string>Assets.xcassets/AppIcon.appiconset</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
</dict> </dict>

View File

@ -97,12 +97,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
set => InternalChild = value; set => InternalChild = value;
} }
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
{
throw new System.NotImplementedException();
}
public override SnapResult FindSnappedPosition(Vector2 screenSpacePosition)
{ {
throw new System.NotImplementedException(); throw new System.NotImplementedException();
} }

View File

@ -5,7 +5,10 @@ using osu.Framework.Graphics;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Skinning.Default;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints namespace osu.Game.Rulesets.Mania.Edit.Blueprints
@ -52,8 +55,29 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{ {
base.UpdateTimeAndPosition(result); base.UpdateTimeAndPosition(result);
if (PlacementActive == PlacementState.Waiting) if (result.Playfield is Column col)
Column = result.Playfield as Column; {
// 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;
} }
} }

View File

@ -1,15 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // 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.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Input; 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.Mania.UI;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -56,28 +55,6 @@ namespace osu.Game.Rulesets.Mania.Edit
protected override Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => protected override Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) =>
Playfield.GetColumnByPosition(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<ManiaHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null) protected override DrawableRuleset<ManiaHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
{ {
drawableRuleset = new DrawableManiaEditorRuleset(ruleset, beatmap, mods); drawableRuleset = new DrawableManiaEditorRuleset(ruleset, beatmap, mods);

View File

@ -37,6 +37,8 @@
</array> </array>
<key>XSAppIconAssets</key> <key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string> <string>Assets.xcassets/AppIcon.appiconset</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
</dict> </dict>

View File

@ -7,6 +7,7 @@ using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Caching; using osu.Framework.Caching;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; 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 snapResult;
return new SnapResult(screenSpacePosition, null); if (snapType.HasFlagFast(SnapType.Grids))
}
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
{
var positionSnap = FindSnappedPosition(screenSpacePosition);
if (positionSnap.ScreenSpacePosition != screenSpacePosition)
return positionSnap;
if (distanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
{ {
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition)); if (distanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition)); {
(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) return base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
{
Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(screenSpacePosition));
return new SnapResult(rectangularPositionSnapGrid.ToScreenSpace(pos), null, PlayfieldAtScreenSpacePosition(screenSpacePosition));
}
return base.FindSnappedPositionAndTime(screenSpacePosition);
} }
private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult) private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult)

View File

@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
private readonly Bindable<Color4> accentColour = new Bindable<Color4>(); private readonly Bindable<Color4> accentColour = new Bindable<Color4>();
private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>(); private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>();
[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; } private DrawableHitObject? drawableObject { get; set; }
[Resolved] [Resolved]

View File

@ -37,6 +37,8 @@
</array> </array>
<key>XSAppIconAssets</key> <key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string> <string>Assets.xcassets/AppIcon.appiconset</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
</dict> </dict>

View File

@ -37,6 +37,8 @@
</array> </array>
<key>XSAppIconAssets</key> <key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string> <string>Assets.xcassets/AppIcon.appiconset</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
</dict> </dict>

View File

@ -863,5 +863,40 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(decoded.Difficulty.OverallDifficulty, Is.EqualTo(1)); 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);
}
}
} }
} }

View File

@ -507,7 +507,7 @@ namespace osu.Game.Tests.Database
using (var stream = storage.GetStream(firstFile.File.GetStoragePath())) using (var stream = storage.GetStream(firstFile.File.GetStoragePath()))
originalLength = stream.Length; originalLength = stream.Length;
using (var stream = storage.GetStream(firstFile.File.GetStoragePath(), FileAccess.Write, FileMode.Create)) using (var stream = storage.CreateFileSafely(firstFile.File.GetStoragePath()))
stream.WriteByte(0); stream.WriteByte(0);
var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm); var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm);

View File

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

View File

@ -185,10 +185,7 @@ namespace osu.Game.Tests.Visual.Editing
private class SnapProvider : IDistanceSnapProvider private class SnapProvider : IDistanceSnapProvider
{ {
public SnapResult FindSnappedPosition(Vector2 screenSpacePosition) => public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.Grids) => new SnapResult(screenSpacePosition, 0);
new SnapResult(screenSpacePosition, null);
public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
public Bindable<double> DistanceSpacingMultiplier { get; } = new BindableDouble(1); public Bindable<double> DistanceSpacingMultiplier { get; } = new BindableDouble(1);

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using NUnit.Framework; using NUnit.Framework;
@ -25,7 +24,7 @@ namespace osu.Game.Tests.Visual.Navigation
if (isDisposing) if (isDisposing)
return; return;
using (var outStream = LocalStorage.GetStream(DatabaseContextFactory.DATABASE_NAME, FileAccess.Write, FileMode.Create)) using (var outStream = LocalStorage.CreateFileSafely(DatabaseContextFactory.DATABASE_NAME))
using (var stream = TestResources.OpenResource(DatabaseContextFactory.DATABASE_NAME)) using (var stream = TestResources.OpenResource(DatabaseContextFactory.DATABASE_NAME))
stream.CopyTo(outStream); stream.CopyTo(outStream);
} }

View File

@ -5,6 +5,8 @@ using System;
using System.Linq; using System.Linq;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net; using System.Net;
using System.Threading;
using JetBrains.Annotations;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -31,7 +33,7 @@ namespace osu.Game.Tests.Visual.Online
[TestFixture] [TestFixture]
public class TestSceneChatOverlayV2 : OsuManualInputManagerTestScene public class TestSceneChatOverlayV2 : OsuManualInputManagerTestScene
{ {
private ChatOverlayV2 chatOverlay; private TestChatOverlayV2 chatOverlay;
private ChannelManager channelManager; private ChannelManager channelManager;
private APIUser testUser; private APIUser testUser;
@ -61,7 +63,7 @@ namespace osu.Game.Tests.Visual.Online
Children = new Drawable[] Children = new Drawable[]
{ {
channelManager, channelManager,
chatOverlay = new ChatOverlayV2 { RelativeSizeAxes = Axes.Both }, chatOverlay = new TestChatOverlayV2 { RelativeSizeAxes = Axes.Both },
}, },
}; };
}); });
@ -365,19 +367,19 @@ namespace osu.Game.Tests.Visual.Online
} }
[Test] [Test]
public void TextBoxRetainsFocus() public void TestTextBoxRetainsFocus()
{ {
AddStep("Show overlay", () => chatOverlay.Show()); AddStep("Show overlay", () => chatOverlay.Show());
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
AddStep("Click drawable channel", () => clickDrawable(currentDrawableChannel));
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
AddStep("Click selector", () => clickDrawable(channelSelectorButton)); AddStep("Click selector", () => clickDrawable(channelSelectorButton));
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
AddStep("Click listing", () => clickDrawable(chatOverlay.ChildrenOfType<ChannelListing>().Single())); AddStep("Click listing", () => clickDrawable(chatOverlay.ChildrenOfType<ChannelListing>().Single()));
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); 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<ChannelList>().Single())); AddStep("Click channel list", () => clickDrawable(chatOverlay.ChildrenOfType<ChannelList>().Single()));
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
AddStep("Click top bar", () => clickDrawable(chatOverlay.ChildrenOfType<ChatOverlayTopBar>().Single())); AddStep("Click top bar", () => clickDrawable(chatOverlay.ChildrenOfType<ChatOverlayTopBar>().Single()));
@ -386,6 +388,34 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("TextBox is not focused", () => InputManager.FocusedDrawable == null); 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 => private bool listingIsVisible =>
chatOverlay.ChildrenOfType<ChannelListing>().Single().State.Value == Visibility.Visible; chatOverlay.ChildrenOfType<ChannelListing>().Single().State.Value == Visibility.Visible;
@ -435,5 +465,35 @@ namespace osu.Game.Tests.Visual.Online
Topic = $"We talk about the number {id} here", Topic = $"We talk about the number {id} here",
Type = ChannelType.Public, Type = ChannelType.Public,
}; };
private class TestChatOverlayV2 : ChatOverlayV2
{
public bool SlowLoading { get; set; }
public SlowLoadingDrawableChannel GetSlowLoadingChannel(Channel channel) => DrawableChannels.OfType<SlowLoadingDrawableChannel>().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);
}
}
} }
} }

View File

@ -55,7 +55,7 @@ namespace osu.Game.Tests.Visual.Settings
.OfType<ISettingsItem>() .OfType<ISettingsItem>()
.OfType<IFilterable>() .OfType<IFilterable>()
.Where(f => !(f is IHasFilterableChildren)) .Where(f => !(f is IHasFilterableChildren))
.All(f => f.FilterTerms.Any(t => t.Contains("scaling"))) .All(f => f.FilterTerms.Any(t => t.ToString().Contains("scaling")))
)); ));
AddAssert("ensure section is current", () => settings.CurrentSection.Value is GraphicsSection); AddAssert("ensure section is current", () => settings.CurrentSection.Value is GraphicsSection);

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using NUnit.Framework; using NUnit.Framework;
@ -127,6 +126,12 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("check info labels count", () => infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Count() == expectedCount); AddAssert("check info labels count", () => infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Count() == expectedCount);
} }
[SetUpSteps]
public void SetUpSteps()
{
AddStep("reset mods", () => SelectedMods.SetDefault());
}
[Test] [Test]
public void TestNullBeatmap() public void TestNullBeatmap()
{ {
@ -147,24 +152,48 @@ namespace osu.Game.Tests.Visual.SongSelect
[Test] [Test]
public void TestBPMUpdates() public void TestBPMUpdates()
{ {
const float bpm = 120; const double bpm = 120;
IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo); IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo);
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / bpm }); beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / bpm });
OsuModDoubleTime doubleTime = null; OsuModDoubleTime doubleTime = null;
selectBeatmap(beatmap); selectBeatmap(beatmap);
checkDisplayedBPM(bpm); checkDisplayedBPM($"{bpm}");
AddStep("select DT", () => SelectedMods.Value = new[] { doubleTime = new OsuModDoubleTime() }); AddStep("select DT", () => SelectedMods.Value = new[] { doubleTime = new OsuModDoubleTime() });
checkDisplayedBPM(bpm * 1.5f); checkDisplayedBPM($"{bpm * 1.5f}");
AddStep("change DT rate", () => doubleTime.SpeedChange.Value = 2); AddStep("change DT rate", () => doubleTime.SpeedChange.Value = 2);
checkDisplayedBPM(bpm * 2); checkDisplayedBPM($"{bpm * 2}");
}
void checkDisplayedBPM(float target) => [TestCase(120, 125, null, "120-125 (mostly 120)")]
AddUntilStep($"displayed bpm is {target}", () => this.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Any( [TestCase(120, 120.6, null, "120-121 (mostly 120)")]
label => label.Statistic.Name == "BPM" && label.Statistic.Content == target.ToString(CultureInfo.InvariantCulture))); [TestCase(120, 120.4, null, "120")]
[TestCase(120, 120.6, "DT", "180-182 (mostly 180)")]
[TestCase(120, 120.4, "DT", "180")]
public void TestVaryingBPM(double commonBpm, double otherBpm, string mod, string expectedDisplay)
{
IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo);
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm });
beatmap.ControlPointInfo.Add(100, new TimingControlPoint { BeatLength = 60 * 1000 / otherBpm });
beatmap.ControlPointInfo.Add(200, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm });
if (mod != null)
AddStep($"select {mod}", () => SelectedMods.Value = new[] { Ruleset.Value.CreateInstance().CreateModFromAcronym(mod) });
selectBeatmap(beatmap);
checkDisplayedBPM(expectedDisplay);
}
private void checkDisplayedBPM(string target)
{
AddUntilStep($"displayed bpm is {target}", () =>
{
var label = infoWedge.DisplayedContent.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Single(l => l.Statistic.Name == "BPM");
return label.Statistic.Content == target;
});
} }
private void setRuleset(RulesetInfo rulesetInfo) private void setRuleset(RulesetInfo rulesetInfo)

View File

@ -0,0 +1,48 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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> legacyImportManager = new Mock<LegacyImportManager>();
[BackgroundDependencyLoader]
private void load()
{
legacyImportManager.Setup(m => m.GetImportCount(It.IsAny<StableContent>(), It.IsAny<CancellationToken>())).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())
}
};
});
}
}
}

View File

@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("results filtered correctly", AddAssert("results filtered correctly",
() => playlistOverlay.ChildrenOfType<PlaylistItem>() () => playlistOverlay.ChildrenOfType<PlaylistItem>()
.Where(item => item.MatchingFilter) .Where(item => item.MatchingFilter)
.All(item => item.FilterTerms.Any(term => term.Contains("10")))); .All(item => item.FilterTerms.Any(term => term.ToString().Contains("10"))));
} }
} }
} }

View File

@ -28,7 +28,7 @@ namespace osu.Game.Tournament.Tests.NonVisual
// ReSharper disable once AccessToDisposedClosure // ReSharper disable once AccessToDisposedClosure
var storage = host.Storage.GetStorageForDirectory(Path.Combine("tournaments", "default")); var storage = host.Storage.GetStorageForDirectory(Path.Combine("tournaments", "default"));
using (var stream = storage.GetStream("bracket.json", FileAccess.Write, FileMode.Create)) using (var stream = storage.CreateFileSafely("bracket.json"))
using (var writer = new StreamWriter(stream)) using (var writer = new StreamWriter(stream))
{ {
writer.Write(@"{ writer.Write(@"{

View File

@ -15,7 +15,7 @@ namespace osu.Game.Tournament.Tests.Screens
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(Storage storage) private void load(Storage storage)
{ {
using (var stream = storage.GetStream("drawings.txt", FileAccess.Write)) using (var stream = storage.CreateFileSafely("drawings.txt"))
using (var writer = new StreamWriter(stream)) using (var writer = new StreamWriter(stream))
{ {
writer.WriteLine("KR : South Korea : KOR"); writer.WriteLine("KR : South Korea : KOR");

View File

@ -45,7 +45,7 @@ namespace osu.Game.Tournament.Models
public void SaveChanges() public void SaveChanges()
{ {
using (var stream = configStorage.GetStream(config_path, FileAccess.Write, FileMode.Create)) using (var stream = configStorage.CreateFileSafely(config_path))
using (var sw = new StreamWriter(stream)) using (var sw = new StreamWriter(stream))
{ {
sw.Write(JsonConvert.SerializeObject(this, sw.Write(JsonConvert.SerializeObject(this,

View File

@ -205,7 +205,7 @@ namespace osu.Game.Tournament.Screens.Drawings
try try
{ {
// Write to drawings_results // Write to drawings_results
using (Stream stream = storage.GetStream(results_filename, FileAccess.Write, FileMode.Create)) using (Stream stream = storage.CreateFileSafely(results_filename))
using (StreamWriter sw = new StreamWriter(stream)) using (StreamWriter sw = new StreamWriter(stream))
{ {
sw.Write(text); sw.Write(text);

View File

@ -321,7 +321,7 @@ namespace osu.Game.Tournament
Converters = new JsonConverter[] { new JsonPointConverter() } Converters = new JsonConverter[] { new JsonPointConverter() }
}); });
using (var stream = storage.GetStream(BRACKET_FILENAME, FileAccess.Write, FileMode.Create)) using (var stream = storage.CreateFileSafely(BRACKET_FILENAME))
using (var sw = new StreamWriter(stream)) using (var sw = new StreamWriter(stream))
sw.Write(serialisedLadder); sw.Write(serialisedLadder);
} }

View File

@ -3,9 +3,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Online; using osu.Game.Online;
@ -89,6 +91,8 @@ namespace osu.Game.Beatmaps.Drawables
private void queueDownloads(string[] sourceFilenames, int? limit = null) private void queueDownloads(string[] sourceFilenames, int? limit = null)
{ {
Debug.Assert(LoadState == LoadState.NotLoaded);
try try
{ {
// Matches osu-stable, in order to provide new users with roughly the same randomised selection of bundled beatmaps. // Matches osu-stable, in order to provide new users with roughly the same randomised selection of bundled beatmaps.

View File

@ -22,7 +22,7 @@ namespace osu.Game.Beatmaps.Formats
{ {
public class LegacyBeatmapEncoder public class LegacyBeatmapEncoder
{ {
public const int LATEST_VERSION = 128; public const int FIRST_LAZER_VERSION = 128;
/// <summary> /// <summary>
/// osu! is generally slower than taiko, so a factor is added to increase /// 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) public void Encode(TextWriter writer)
{ {
writer.WriteLine($"osu file format v{LATEST_VERSION}"); writer.WriteLine($"osu file format v{FIRST_LAZER_VERSION}");
writer.WriteLine(); writer.WriteLine();
handleGeneral(writer); handleGeneral(writer);

View File

@ -111,6 +111,18 @@ namespace osu.Game.Collections
public Action<Notification> PostNotification { protected get; set; } public Action<Notification> PostNotification { protected get; set; }
public Task<int> 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;
});
}
/// <summary> /// <summary>
/// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future. /// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future.
/// </summary> /// </summary>

View File

@ -37,7 +37,7 @@ namespace osu.Game.Database
{ {
string filename = $"{item.GetDisplayString().GetValidArchiveContentFilename()}{FileExtension}"; string filename = $"{item.GetDisplayString().GetValidArchiveContentFilename()}{FileExtension}";
using (var stream = exportStorage.GetStream(filename, FileAccess.Write, FileMode.Create)) using (var stream = exportStorage.CreateFileSafely(filename))
ExportModelTo(item, stream); ExportModelTo(item, stream);
exportStorage.PresentFileExternally(filename); exportStorage.PresentFileExternally(filename);

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework; using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -36,22 +37,66 @@ namespace osu.Game.Database
[Resolved] [Resolved]
private CollectionManager collections { get; set; } private CollectionManager collections { get; set; }
[Resolved] [Resolved(canBeNull: true)]
private OsuGame game { get; set; } private OsuGame game { get; set; }
[Resolved] [Resolved]
private IDialogOverlay dialogOverlay { get; set; } private IDialogOverlay dialogOverlay { get; set; }
[Resolved(CanBeNull = true)] [Resolved(canBeNull: true)]
private DesktopGameHost desktopGameHost { get; set; } private DesktopGameHost desktopGameHost { get; set; }
private StableStorage cachedStorage; private StableStorage cachedStorage;
public bool SupportsImportFromStable => RuntimeInfo.IsDesktop; 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<int> 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<string>(TaskCreationOptions.RunContinuationsAsynchronously);
Schedule(() => dialogOverlay.Push(new StableDirectoryLocationDialog(taskCompletionSource)));
string stablePath = await taskCompletionSource.Task.ConfigureAwait(false);
UpdateStorage(stablePath);
stableStorage = GetCurrentStableStorage();
}
var importTasks = new List<Task>(); var importTasks = new List<Task>();
Task beatmapImportTask = Task.CompletedTask; Task beatmapImportTask = Task.CompletedTask;
@ -70,20 +115,16 @@ namespace osu.Game.Database
await Task.WhenAll(importTasks.ToArray()).ConfigureAwait(false); await Task.WhenAll(importTasks.ToArray()).ConfigureAwait(false);
} }
private async Task<StableStorage> getStableStorage() public StableStorage GetCurrentStableStorage()
{ {
if (cachedStorage != null) if (cachedStorage != null)
return cachedStorage; return cachedStorage;
var stableStorage = game.GetStorageForStableInstall(); var stableStorage = game?.GetStorageForStableInstall();
if (stableStorage != null) if (stableStorage != null)
return cachedStorage = stableStorage; return cachedStorage = stableStorage;
var taskCompletionSource = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously); return null;
Schedule(() => dialogOverlay.Push(new StableDirectoryLocationDialog(taskCompletionSource)));
string stablePath = await taskCompletionSource.Task.ConfigureAwait(false);
return cachedStorage = new StableStorage(stablePath, desktopGameHost);
} }
} }

View File

@ -24,8 +24,14 @@ namespace osu.Game.Database
/// <summary> /// <summary>
/// Select paths to import from stable where all paths should be absolute. Default implementation iterates all directories in <see cref="ImportFromStablePath"/>. /// Select paths to import from stable where all paths should be absolute. Default implementation iterates all directories in <see cref="ImportFromStablePath"/>.
/// </summary> /// </summary>
protected virtual IEnumerable<string> GetStableImportPaths(Storage storage) => storage.GetDirectories(ImportFromStablePath) protected virtual IEnumerable<string> GetStableImportPaths(Storage storage)
.Select(path => storage.GetFullPath(path)); {
if (!storage.ExistsDirectory(ImportFromStablePath))
return Enumerable.Empty<string>();
return storage.GetDirectories(ImportFromStablePath)
.Select(path => storage.GetFullPath(path));
}
protected readonly IModelImporter<TModel> Importer; protected readonly IModelImporter<TModel> Importer;
@ -34,6 +40,8 @@ namespace osu.Game.Database
Importer = importer; Importer = importer;
} }
public Task<int> GetAvailableCount(StableStorage stableStorage) => Task.Run(() => GetStableImportPaths(PrepareStableStorage(stableStorage)).Count());
public Task ImportFromStableAsync(StableStorage stableStorage) public Task ImportFromStableAsync(StableStorage stableStorage)
{ {
var storage = PrepareStableStorage(stableStorage); var storage = PrepareStableStorage(stableStorage);

View File

@ -15,8 +15,14 @@ namespace osu.Game.Database
protected override string ImportFromStablePath => Path.Combine("Data", "r"); protected override string ImportFromStablePath => Path.Combine("Data", "r");
protected override IEnumerable<string> GetStableImportPaths(Storage storage) protected override IEnumerable<string> 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<string>();
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<ScoreInfo> importer) public LegacyScoreImporter(IModelImporter<ScoreInfo> importer)
: base(importer) : base(importer)

View File

@ -242,7 +242,7 @@ namespace osu.Game.Database
storage.Delete(Filename); storage.Delete(Filename);
using (var inputStream = storage.GetStream(recoveryFilename)) using (var inputStream = storage.GetStream(recoveryFilename))
using (var outputStream = storage.GetStream(Filename, FileAccess.Write, FileMode.Create)) using (var outputStream = storage.CreateFileSafely(Filename))
inputStream.CopyTo(outputStream); inputStream.CopyTo(outputStream);
storage.Delete(recoveryFilename); storage.Delete(recoveryFilename);

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -118,7 +117,7 @@ namespace osu.Game.Graphics
if (filename == null) return; if (filename == null) return;
using (var stream = storage.GetStream(filename, FileAccess.Write)) using (var stream = storage.CreateFileSafely(filename))
{ {
switch (screenshotFormat.Value) switch (screenshotFormat.Value)
{ {

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Backgrounds;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
@ -27,7 +28,7 @@ namespace osu.Game.Graphics.UserInterface
}); });
} }
public virtual IEnumerable<string> FilterTerms => new[] { Text.ToString() }; public virtual IEnumerable<LocalisableString> FilterTerms => new[] { Text };
public bool MatchingFilter public bool MatchingFilter
{ {

View File

@ -6,6 +6,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
namespace osu.Game.Graphics.UserInterfaceV2 namespace osu.Game.Graphics.UserInterfaceV2
@ -25,7 +26,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
set => Component.ReadOnly = value; set => Component.ReadOnly = value;
} }
public string PlaceholderText public LocalisableString PlaceholderText
{ {
set => Component.PlaceholderText = value; set => Component.PlaceholderText = value;
} }

View File

@ -10,6 +10,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
namespace osu.Game.Graphics.UserInterfaceV2 namespace osu.Game.Graphics.UserInterfaceV2
{ {
@ -44,8 +45,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
internal class Background : CompositeDrawable internal class Background : CompositeDrawable
{ {
[BackgroundDependencyLoader] [BackgroundDependencyLoader(true)]
private void load(OsuColour colours) private void load(OverlayColourProvider overlayColourProvider, OsuColour colours)
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@ -54,7 +55,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
InternalChild = new Box InternalChild = new Box
{ {
Colour = colours.GreySeaFoamDarker, Colour = overlayColourProvider?.Background5 ?? colours.GreySeaFoamDarker,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}; };
} }

View File

@ -6,6 +6,7 @@ using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays; using osu.Game.Overlays;
@ -39,7 +40,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
private void updateCornerRadius() => Content.CornerRadius = DrawHeight / 2; private void updateCornerRadius() => Content.CornerRadius = DrawHeight / 2;
public virtual IEnumerable<string> FilterTerms => new[] { Text.ToString() }; public virtual IEnumerable<LocalisableString> FilterTerms => new[] { Text };
public bool MatchingFilter public bool MatchingFilter
{ {

View File

@ -70,6 +70,8 @@ namespace osu.Game.IO
public override Stream GetStream(string path, FileAccess access = FileAccess.Read, FileMode mode = FileMode.OpenOrCreate) => public override Stream GetStream(string path, FileAccess access = FileAccess.Read, FileMode mode = FileMode.OpenOrCreate) =>
UnderlyingStorage.GetStream(MutatePath(path), access, mode); UnderlyingStorage.GetStream(MutatePath(path), access, mode);
public override void Move(string from, string to) => UnderlyingStorage.Move(MutatePath(from), MutatePath(to));
public override bool OpenFileExternally(string filename) => UnderlyingStorage.OpenFileExternally(MutatePath(filename)); public override bool OpenFileExternally(string filename) => UnderlyingStorage.OpenFileExternally(MutatePath(filename));
public override bool PresentFileExternally(string filename) => UnderlyingStorage.PresentFileExternally(MutatePath(filename)); public override bool PresentFileExternally(string filename) => UnderlyingStorage.PresentFileExternally(MutatePath(filename));

View File

@ -69,6 +69,26 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString SelectAll => new TranslatableString(getKey(@"select_all"), @"Select All"); public static LocalisableString SelectAll => new TranslatableString(getKey(@"select_all"), @"Select All");
/// <summary>
/// "Beatmaps"
/// </summary>
public static LocalisableString Beatmaps => new TranslatableString(getKey(@"beatmaps"), @"Beatmaps");
/// <summary>
/// "Scores"
/// </summary>
public static LocalisableString Scores => new TranslatableString(getKey(@"scores"), @"Scores");
/// <summary>
/// "Skins"
/// </summary>
public static LocalisableString Skins => new TranslatableString(getKey(@"skins"), @"Skins");
/// <summary>
/// "Collections"
/// </summary>
public static LocalisableString Collections => new TranslatableString(getKey(@"collections"), @"Collections");
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -0,0 +1,56 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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";
/// <summary>
/// "Import"
/// </summary>
public static LocalisableString Header => new TranslatableString(getKey(@"header"), @"Import");
/// <summary>
/// "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."
/// </summary>
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.");
/// <summary>
/// "previous osu! install"
/// </summary>
public static LocalisableString LocateDirectoryLabel => new TranslatableString(getKey(@"locate_directory_label"), @"previous osu! install");
/// <summary>
/// "Click to locate a previous osu! install"
/// </summary>
public static LocalisableString LocateDirectoryPlaceholder => new TranslatableString(getKey(@"locate_directory_placeholder"), @"Click to locate a previous osu! install");
/// <summary>
/// "Import content from previous version"
/// </summary>
public static LocalisableString ImportButton => new TranslatableString(getKey(@"import_button"), @"Import content from previous version");
/// <summary>
/// "Your import will continue in the background. Check on its progress in the notifications sidebar!"
/// </summary>
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!");
/// <summary>
/// "calculating..."
/// </summary>
public static LocalisableString Calculating => new TranslatableString(getKey(@"calculating"), @"calculating...");
/// <summary>
/// "{0} items"
/// </summary>
public static LocalisableString Items(int arg0) => new TranslatableString(getKey(@"items"), @"{0} item(s)", arg0);
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -1207,6 +1207,8 @@ namespace osu.Game
Current = newScreen?.GetType().ReadableName(), Current = newScreen?.GetType().ReadableName(),
Previous = current?.GetType().ReadableName(), Previous = current?.GetType().ReadableName(),
}; };
scope.SetTag(@"screen", newScreen?.GetType().ReadableName() ?? @"none");
}); });
switch (newScreen) switch (newScreen)

View File

@ -243,7 +243,7 @@ namespace osu.Game
{ {
if (source != null) if (source != null)
{ {
using (var destination = Storage.GetStream(Path.Combine(backup_folder, $"collection.{migration}.db"), FileAccess.Write, FileMode.CreateNew)) using (var destination = Storage.CreateFileSafely(Path.Combine(backup_folder, $"collection.{migration}.db")))
source.CopyTo(destination); source.CopyTo(destination);
} }
} }

View File

@ -12,6 +12,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -28,7 +29,7 @@ namespace osu.Game.Overlays.Chat.Listing
public readonly Channel Channel; public readonly Channel Channel;
public bool FilteringActive { get; set; } public bool FilteringActive { get; set; }
public IEnumerable<string> FilterTerms => new[] { Channel.Name, Channel.Topic ?? string.Empty }; public IEnumerable<LocalisableString> FilterTerms => new LocalisableString[] { Channel.Name, Channel.Topic ?? string.Empty };
public bool MatchingFilter { set => this.FadeTo(value ? 1f : 0f, 100); } public bool MatchingFilter { set => this.FadeTo(value ? 1f : 0f, 100); }
private Box hoverBox = null!; private Box hoverBox = null!;

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Online.Chat; using osu.Game.Online.Chat;
@ -36,7 +37,7 @@ namespace osu.Game.Overlays.Chat.Selection
private Color4 topicColour; private Color4 topicColour;
private Color4 hoverColour; private Color4 hoverColour;
public IEnumerable<string> FilterTerms => new[] { Channel.Name, Channel.Topic ?? string.Empty }; public IEnumerable<LocalisableString> FilterTerms => new LocalisableString[] { Channel.Name, Channel.Topic ?? string.Empty };
public bool MatchingFilter public bool MatchingFilter
{ {

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Online.Chat; using osu.Game.Online.Chat;
@ -18,7 +19,7 @@ namespace osu.Game.Overlays.Chat.Selection
public readonly FillFlowContainer<ChannelListItem> ChannelFlow; public readonly FillFlowContainer<ChannelListItem> ChannelFlow;
public IEnumerable<IFilterable> FilterableChildren => ChannelFlow.Children; public IEnumerable<IFilterable> FilterableChildren => ChannelFlow.Children;
public IEnumerable<string> FilterTerms => Array.Empty<string>(); public IEnumerable<LocalisableString> FilterTerms => Array.Empty<LocalisableString>();
public bool MatchingFilter public bool MatchingFilter
{ {

View File

@ -39,8 +39,11 @@ namespace osu.Game.Overlays
private ChatTextBar textBar = null!; private ChatTextBar textBar = null!;
private Container<ChatOverlayDrawableChannel> currentChannelContainer = null!; private Container<ChatOverlayDrawableChannel> currentChannelContainer = null!;
private readonly BindableFloat chatHeight = new BindableFloat(); private readonly Dictionary<Channel, ChatOverlayDrawableChannel> loadedChannels = new Dictionary<Channel, ChatOverlayDrawableChannel>();
protected IEnumerable<DrawableChannel> DrawableChannels => loadedChannels.Values;
private readonly BindableFloat chatHeight = new BindableFloat();
private bool isDraggingTopBar; private bool isDraggingTopBar;
private float dragStartChatHeight; private float dragStartChatHeight;
@ -253,38 +256,76 @@ namespace osu.Game.Overlays
if (newChannel is ChannelListing.ChannelListingChannel) if (newChannel is ChannelListing.ChannelListingChannel)
{ {
channelListing.State.Value = Visibility.Visible; currentChannelContainer.Clear(false);
channelListing.Show();
textBar.ShowSearch.Value = true; textBar.ShowSearch.Value = true;
} }
else else
{ {
channelListing.State.Value = Visibility.Hidden; channelListing.Hide();
textBar.ShowSearch.Value = false; textBar.ShowSearch.Value = false;
loading.Show(); if (loadedChannels.ContainsKey(newChannel))
LoadComponentAsync(new ChatOverlayDrawableChannel(newChannel), loaded =>
{ {
currentChannelContainer.Clear(); currentChannelContainer.Clear(false);
currentChannelContainer.Add(loaded); currentChannelContainer.Add(loadedChannels[newChannel]);
loading.Hide(); }
}); 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) private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args)
{ {
switch (args.Action) switch (args.Action)
{ {
case NotifyCollectionChangedAction.Add: case NotifyCollectionChangedAction.Add:
IEnumerable<Channel> newChannels = filterChannels(args.NewItems); IEnumerable<Channel> newChannels = filterChannels(args.NewItems);
foreach (var channel in newChannels) foreach (var channel in newChannels)
channelList.AddChannel(channel); channelList.AddChannel(channel);
break; break;
case NotifyCollectionChangedAction.Remove: case NotifyCollectionChangedAction.Remove:
IEnumerable<Channel> leftChannels = filterChannels(args.OldItems); IEnumerable<Channel> leftChannels = filterChannels(args.OldItems);
foreach (var channel in leftChannels) foreach (var channel in leftChannels)
{
channelList.RemoveChannel(channel); 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; break;
} }
} }

View File

@ -21,6 +21,8 @@ namespace osu.Game.Overlays.FirstRunSetup
protected const float CONTENT_FONT_SIZE = 16; protected const float CONTENT_FONT_SIZE = 16;
protected const float CONTENT_PADDING = 30;
protected const float HEADER_FONT_SIZE = 24; protected const float HEADER_FONT_SIZE = 24;
[Resolved] [Resolved]
@ -41,7 +43,7 @@ namespace osu.Game.Overlays.FirstRunSetup
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = 30 }, Padding = new MarginPadding { Horizontal = CONTENT_PADDING },
Children = new Drawable[] Children = new Drawable[]
{ {
new OsuSpriteText new OsuSpriteText

View File

@ -0,0 +1,103 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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);
}
}
}

View File

@ -14,8 +14,6 @@ using osu.Game.Beatmaps.Drawables;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Online; using osu.Game.Online;
using osuTK; using osuTK;
@ -27,7 +25,6 @@ namespace osu.Game.Overlays.FirstRunSetup
public class ScreenBeatmaps : FirstRunSetupScreen public class ScreenBeatmaps : FirstRunSetupScreen
{ {
private ProgressRoundedButton downloadBundledButton = null!; private ProgressRoundedButton downloadBundledButton = null!;
private ProgressRoundedButton importBeatmapsButton = null!;
private ProgressRoundedButton downloadTutorialButton = null!; private ProgressRoundedButton downloadTutorialButton = null!;
private OsuTextFlowContainer currentlyLoadedBeatmaps = null!; private OsuTextFlowContainer currentlyLoadedBeatmaps = null!;
@ -43,8 +40,8 @@ namespace osu.Game.Overlays.FirstRunSetup
private IDisposable? beatmapSubscription; private IDisposable? beatmapSubscription;
[BackgroundDependencyLoader(permitNulls: true)] [BackgroundDependencyLoader]
private void load(LegacyImportManager? legacyImportManager) private void load()
{ {
Vector2 buttonSize = new Vector2(400, 50); Vector2 buttonSize = new Vector2(400, 50);
@ -106,32 +103,6 @@ namespace osu.Game.Overlays.FirstRunSetup
Action = downloadBundled Action = downloadBundled
}, },
new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE)) 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, Colour = OverlayColourProvider.Content1,
Text = FirstRunSetupBeatmapScreenStrings.ObtainMoreBeatmaps, Text = FirstRunSetupBeatmapScreenStrings.ObtainMoreBeatmaps,
@ -214,45 +185,5 @@ namespace osu.Game.Overlays.FirstRunSetup
downloadBundledButton.SetProgress(progress, true); 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);
}
}
} }
} }

View File

@ -0,0 +1,267 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<ImportCheckbox> contentCheckboxes => Content.Children.OfType<ImportCheckbox>();
[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<string> HandledExtensions { get; } = new[] { string.Empty };
private readonly Bindable<DirectoryInfo> currentDirectory = new Bindable<DirectoryInfo>();
[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<DirectoryInfo> 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<DirectoryInfo> currentDirectory)
{
Child = new Container
{
Size = new Vector2(600, 400),
Child = new OsuDirectorySelector(currentDirectory.Value?.FullName)
{
RelativeSizeAxes = Axes.Both,
CurrentPath = { BindTarget = currentDirectory }
},
};
}
}
}
}
}

View File

@ -4,12 +4,14 @@
#nullable enable #nullable enable
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
@ -17,6 +19,7 @@ using osu.Framework.Localisation;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
@ -55,18 +58,10 @@ namespace osu.Game.Overlays
/// </summary> /// </summary>
public FirstRunSetupScreen? CurrentScreen => (FirstRunSetupScreen?)stack?.CurrentScreen; public FirstRunSetupScreen? CurrentScreen => (FirstRunSetupScreen?)stack?.CurrentScreen;
private readonly Type[] steps = private readonly List<Type> steps = new List<Type>();
{
typeof(ScreenWelcome),
typeof(ScreenBeatmaps),
typeof(ScreenUIScale),
typeof(ScreenBehaviour),
};
private Container screenContent = null!; private Container screenContent = null!;
private Bindable<OverlayActivation>? overlayActivationMode;
private Container content = null!; private Container content = null!;
private LoadingSpinner loading = null!; private LoadingSpinner loading = null!;
@ -77,15 +72,22 @@ namespace osu.Game.Overlays
{ {
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader(permitNulls: true)]
private void load(OsuColour colours) 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.Title = FirstRunSetupOverlayStrings.FirstRunSetupTitle;
Header.Description = FirstRunSetupOverlayStrings.FirstRunSetupDescription; Header.Description = FirstRunSetupOverlayStrings.FirstRunSetupDescription;
MainAreaContent.AddRange(new Drawable[] MainAreaContent.AddRange(new Drawable[]
{ {
content = new Container content = new PopoverContainer
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = 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. // if we are valid for display, only do so after reaching the main menu.
performer.PerformFromScreen(screen => performer.PerformFromScreen(screen =>
{ {
MainMenu menu = (MainMenu)screen; // Hides the toolbar for us.
if (screen is MainMenu menu)
// Eventually I'd like to replace this with a better method that doesn't access the screen. menu.ReturnToOsuLogo();
// 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;
}
base.Show(); base.Show();
}, new[] { typeof(MainMenu) }); }, new[] { typeof(MainMenu) });
@ -253,13 +248,6 @@ namespace osu.Game.Overlays
content.ScaleTo(0.99f, 400, Easing.OutQuint); 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) if (currentStepIndex != null)
{ {
notificationOverlay.Post(new SimpleNotification notificationOverlay.Post(new SimpleNotification
@ -313,7 +301,7 @@ namespace osu.Game.Overlays
currentStepIndex++; currentStepIndex++;
if (currentStepIndex < steps.Length) if (currentStepIndex < steps.Count)
{ {
var nextScreen = (Screen)Activator.CreateInstance(steps[currentStepIndex.Value]); var nextScreen = (Screen)Activator.CreateInstance(steps[currentStepIndex.Value]);
@ -345,7 +333,7 @@ namespace osu.Game.Overlays
return; return;
bool isFirstStep = currentStepIndex == 0; bool isFirstStep = currentStepIndex == 0;
bool isLastStep = currentStepIndex == steps.Length - 1; bool isLastStep = currentStepIndex == steps.Count - 1;
if (isFirstStep) if (isFirstStep)
{ {

View File

@ -428,9 +428,8 @@ namespace osu.Game.Overlays.Mods
base.PopIn(); base.PopIn();
multiplierDisplay? multiplierDisplay?
.Delay(fade_in_duration * 0.65f) .FadeIn(fade_in_duration, Easing.OutQuint)
.FadeIn(fade_in_duration / 2, Easing.OutQuint) .MoveToY(0, fade_in_duration, Easing.OutQuint);
.ScaleTo(1, fade_in_duration, Easing.OutElastic);
int nonFilteredColumnCount = 0; int nonFilteredColumnCount = 0;
@ -465,7 +464,7 @@ namespace osu.Game.Overlays.Mods
multiplierDisplay? multiplierDisplay?
.FadeOut(fade_out_duration / 2, Easing.OutQuint) .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; int nonFilteredColumnCount = 0;

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -113,7 +114,7 @@ namespace osu.Game.Overlays.Music
} }
} }
public IEnumerable<string> FilterTerms => Model.PerformRead(m => m.Metadata.GetSearchableTerms()); public IEnumerable<LocalisableString> FilterTerms => Model.PerformRead(m => m.Metadata.GetSearchableTerms()).Select(s => (LocalisableString)s).ToArray();
private bool matchingFilter = true; private bool matchingFilter = true;

View File

@ -16,7 +16,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
{ {
protected override LocalisableString Header => AudioSettingsStrings.OffsetHeader; protected override LocalisableString Header => AudioSettingsStrings.OffsetHeader;
public override IEnumerable<string> FilterTerms => base.FilterTerms.Concat(new[] { "universal", "uo", "timing" }); public override IEnumerable<LocalisableString> FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "universal", "uo", "timing" });
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config) private void load(OsuConfigManager config)

View File

@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Settings.Sections
Icon = FontAwesome.Solid.VolumeUp Icon = FontAwesome.Solid.VolumeUp
}; };
public override IEnumerable<string> FilterTerms => base.FilterTerms.Concat(new[] { "sound" }); public override IEnumerable<LocalisableString> FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "sound" });
public AudioSection() public AudioSection()
{ {

View File

@ -14,7 +14,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
{ {
protected override LocalisableString Header => GameplaySettingsStrings.ModsHeader; protected override LocalisableString Header => GameplaySettingsStrings.ModsHeader;
public override IEnumerable<string> FilterTerms => base.FilterTerms.Concat(new[] { "mod" }); public override IEnumerable<LocalisableString> FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "mod" });
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config) private void load(OsuConfigManager config)

View File

@ -15,6 +15,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -67,7 +68,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
private Bindable<bool> isDefault { get; } = new BindableBool(true); private Bindable<bool> isDefault { get; } = new BindableBool(true);
public IEnumerable<string> FilterTerms => bindings.Select(b => keyCombinationProvider.GetReadableString(b.KeyCombination)).Prepend(text.Text.ToString()); public IEnumerable<LocalisableString> FilterTerms => bindings.Select(b => (LocalisableString)keyCombinationProvider.GetReadableString(b.KeyCombination)).Prepend(text.Text);
public KeyBindingRow(object action, List<RealmKeyBinding> bindings) public KeyBindingRow(object action, List<RealmKeyBinding> bindings)
{ {

View File

@ -6,6 +6,7 @@ using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -74,6 +75,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input
} }
// Empty FilterTerms so that the ResetButton is visible only when the whole subsection is visible. // Empty FilterTerms so that the ResetButton is visible only when the whole subsection is visible.
public override IEnumerable<string> FilterTerms => Enumerable.Empty<string>(); public override IEnumerable<LocalisableString> FilterTerms => Enumerable.Empty<LocalisableString>();
} }
} }

View File

@ -20,13 +20,12 @@ namespace osu.Game.Overlays.Settings
public LocalisableString TooltipText { get; set; } public LocalisableString TooltipText { get; set; }
public override IEnumerable<string> FilterTerms public override IEnumerable<LocalisableString> FilterTerms
{ {
get get
{ {
if (TooltipText != default) if (TooltipText != default)
// TODO: this won't work as intended once the tooltip text is translated. return base.FilterTerms.Append(TooltipText);
return base.FilterTerms.Append(TooltipText.ToString());
return base.FilterTerms; return base.FilterTerms;
} }

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings namespace osu.Game.Overlays.Settings
@ -25,7 +26,7 @@ namespace osu.Game.Overlays.Settings
set => Control.ItemSource = value; set => Control.ItemSource = value;
} }
public override IEnumerable<string> FilterTerms => base.FilterTerms.Concat(Control.Items.Select(i => i.ToString())); public override IEnumerable<LocalisableString> FilterTerms => base.FilterTerms.Concat(Control.Items.Select(i => (LocalisableString)i.ToString()));
protected sealed override Drawable CreateControl() => CreateDropdown(); protected sealed override Drawable CreateControl() => CreateDropdown();

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -98,13 +99,13 @@ namespace osu.Game.Overlays.Settings
set => controlWithCurrent.Current = value; set => controlWithCurrent.Current = value;
} }
public virtual IEnumerable<string> FilterTerms public virtual IEnumerable<LocalisableString> FilterTerms
{ {
get get
{ {
var keywords = new List<string>(Keywords ?? Array.Empty<string>()) var keywords = new List<LocalisableString>(Keywords?.Select(k => (LocalisableString)k) ?? Array.Empty<LocalisableString>())
{ {
LabelText.ToString() LabelText
}; };
if (HasClassicDefault) if (HasClassicDefault)

View File

@ -32,7 +32,7 @@ namespace osu.Game.Overlays.Settings
public abstract LocalisableString Header { get; } public abstract LocalisableString Header { get; }
public IEnumerable<IFilterable> FilterableChildren => Children.OfType<IFilterable>(); public IEnumerable<IFilterable> FilterableChildren => Children.OfType<IFilterable>();
public virtual IEnumerable<string> FilterTerms => new[] { Header.ToString() }; public virtual IEnumerable<LocalisableString> FilterTerms => new[] { Header };
public const int ITEM_SPACING = 14; public const int ITEM_SPACING = 14;

View File

@ -25,11 +25,7 @@ namespace osu.Game.Overlays.Settings
public IEnumerable<IFilterable> FilterableChildren => Children.OfType<IFilterable>(); public IEnumerable<IFilterable> FilterableChildren => Children.OfType<IFilterable>();
// FilterTerms should contains both original string and localised string for user to search. public virtual IEnumerable<LocalisableString> FilterTerms => new[] { Header };
// Since LocalisableString is unable to get original string at this time (2021-08-14),
// only call .ToString() to use localised one.
// TODO: Update here when FilterTerms accept LocalisableString.
public virtual IEnumerable<string> FilterTerms => new[] { Header.ToString() };
public bool MatchingFilter public bool MatchingFilter
{ {

View File

@ -7,6 +7,7 @@ using System.Collections.Specialized;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input; using osu.Framework.Input;
@ -361,20 +362,23 @@ namespace osu.Game.Rulesets.Edit
/// <returns>The most relevant <see cref="Playfield"/>.</returns> /// <returns>The most relevant <see cref="Playfield"/>.</returns>
protected virtual Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => drawableRulesetWrapper.Playfield; 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); var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
double? targetTime = null; 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 // apply beat snapping
targetTime = BeatSnapProvider.SnapTime(targetTime.Value); targetTime = BeatSnapProvider.SnapTime(targetTime.Value);
// convert back to screen space // convert back to screen space
screenSpacePosition = scrollingPlayfield.ScreenSpacePositionAtTime(targetTime.Value); screenSpacePosition = scrollingPlayfield.ScreenSpacePositionAtTime(targetTime.Value);
}
} }
return new SnapResult(screenSpacePosition, targetTime, playfield); return new SnapResult(screenSpacePosition, targetTime, playfield);
@ -414,10 +418,7 @@ namespace osu.Game.Rulesets.Edit
#region IPositionSnapProvider #region IPositionSnapProvider
public abstract SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition); public abstract SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All);
public virtual SnapResult FindSnappedPosition(Vector2 screenSpacePosition) =>
new SnapResult(screenSpacePosition, null);
#endregion #endregion
} }

View File

@ -8,7 +8,6 @@ namespace osu.Game.Rulesets.Edit
{ {
/// <summary> /// <summary>
/// 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. /// 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.
/// </summary> /// </summary>
[Cached] [Cached]
public interface IPositionSnapProvider public interface IPositionSnapProvider
@ -16,18 +15,9 @@ namespace osu.Game.Rulesets.Edit
/// <summary> /// <summary>
/// Given a position, find a valid time and position snap. /// Given a position, find a valid time and position snap.
/// </summary> /// </summary>
/// <remarks>
/// This call should be equivalent to running <see cref="FindSnappedPosition"/> with any additional logic that can be performed without the time immutability restriction.
/// </remarks>
/// <param name="screenSpacePosition">The screen-space position to be snapped.</param> /// <param name="screenSpacePosition">The screen-space position to be snapped.</param>
/// <param name="snapType">The type of snapping to apply.</param>
/// <returns>The time and position post-snapping.</returns> /// <returns>The time and position post-snapping.</returns>
SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition); SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All);
/// <summary>
/// Given a position, find a valid position snap, without changing the time value.
/// </summary>
/// <param name="screenSpacePosition">The screen-space position to be snapped.</param>
/// <returns>The position post-snapping. Time will always be null.</returns>
SnapResult FindSnappedPosition(Vector2 screenSpacePosition);
} }
} }

View File

@ -0,0 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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,
}
}

View File

@ -336,10 +336,14 @@ namespace osu.Game.Rulesets.Objects.Legacy
while (++endIndex < vertices.Length - endPointLength) 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) if (vertices[endIndex].Position != vertices[endIndex - 1].Position)
continue; 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. // The last control point of each segment is not allowed to start a new implicit segment.
if (endIndex == vertices.Length - endPointLength - 1) if (endIndex == vertices.Length - endPointLength - 1)
continue; continue;

View File

@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
public new ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)base.HitObjectContainer; public new ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)base.HitObjectContainer;
[Resolved] [Resolved]
protected IScrollingInfo ScrollingInfo { get; private set; } public IScrollingInfo ScrollingInfo { get; private set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()

View File

@ -486,7 +486,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
Vector2 originalPosition = movementBlueprintOriginalPositions[i]; Vector2 originalPosition = movementBlueprintOriginalPositions[i];
var testPosition = originalPosition + distanceTravelled; var testPosition = originalPosition + distanceTravelled;
var positionalResult = snapProvider.FindSnappedPosition(testPosition); var positionalResult = snapProvider.FindSnappedPositionAndTime(testPosition, SnapType.NearbyObjects);
if (positionalResult.ScreenSpacePosition == testPosition) continue; if (positionalResult.ScreenSpacePosition == testPosition) continue;
@ -505,7 +505,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
Vector2 movePosition = movementBlueprintOriginalPositions.First() + distanceTravelled; Vector2 movePosition = movementBlueprintOriginalPositions.First() + distanceTravelled;
// Retrieve a snapped position. // Retrieve a snapped position.
var result = snapProvider?.FindSnappedPositionAndTime(movePosition); var result = snapProvider?.FindSnappedPositionAndTime(movePosition, ~SnapType.NearbyObjects);
if (result == null) if (result == null)
{ {

View File

@ -154,7 +154,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void updateBankPlaceholderText(IEnumerable<HitObject> objects) private void updateBankPlaceholderText(IEnumerable<HitObject> objects)
{ {
string? commonBank = getCommonBank(objects.Select(h => h.SampleControlPoint).ToArray()); 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<HitObject> objects, int? newVolume) private void updateVolumeFor(IEnumerable<HitObject> objects, int? newVolume)

View File

@ -303,10 +303,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
/// </summary> /// </summary>
public double VisibleRange => track.Length / Zoom; public double VisibleRange => track.Length / Zoom;
public SnapResult FindSnappedPosition(Vector2 screenSpacePosition) => public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) =>
new SnapResult(screenSpacePosition, null);
public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) =>
new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition)))); new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition))));
private double getTimeFromPosition(Vector2 localPosition) => private double getTimeFromPosition(Vector2 localPosition) =>

View File

@ -11,11 +11,8 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osuTK; using osuTK;
@ -24,7 +21,7 @@ namespace osu.Game.Screens.Edit.Setup
/// <summary> /// <summary>
/// A labelled textbox which reveals an inline file chooser when clicked. /// A labelled textbox which reveals an inline file chooser when clicked.
/// </summary> /// </summary>
internal class FileChooserLabelledTextBox : LabelledTextBox, ICanAcceptFiles, IHasPopover internal class FileChooserLabelledTextBox : LabelledTextBoxWithPopover, ICanAcceptFiles
{ {
private readonly string[] handledExtensions; private readonly string[] handledExtensions;
@ -40,16 +37,6 @@ namespace osu.Game.Screens.Edit.Setup
this.handledExtensions = handledExtensions; 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() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
@ -81,27 +68,7 @@ namespace osu.Game.Screens.Edit.Setup
game.UnregisterImportHandler(this); game.UnregisterImportHandler(this);
} }
internal class FileChooserOsuTextBox : OsuTextBox public override Popover GetPopover() => new FileChooserPopover(handledExtensions, currentFile);
{
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);
private class FileChooserPopover : OsuPopover private class FileChooserPopover : OsuPopover
{ {

View File

@ -0,0 +1,52 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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);
}
}
}
}

View File

@ -150,6 +150,8 @@ namespace osu.Game.Screens.Menu
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private IPerformFromScreenRunner performer { get; set; } private IPerformFromScreenRunner performer { get; set; }
public void ReturnToOsuLogo() => Buttons.State = ButtonSystemState.Initial;
private void confirmAndExit() private void confirmAndExit()
{ {
if (exitConfirmed) return; if (exitConfirmed) return;

View File

@ -80,7 +80,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
matchingFilter &= criteria.Ruleset == null || r.Room.PlaylistItemStats.Value?.RulesetIDs.Any(id => id == criteria.Ruleset.OnlineID) != false; matchingFilter &= criteria.Ruleset == null || r.Room.PlaylistItemStats.Value?.RulesetIDs.Any(id => id == criteria.Ruleset.OnlineID) != false;
if (!string.IsNullOrEmpty(criteria.SearchString)) if (!string.IsNullOrEmpty(criteria.SearchString))
matchingFilter &= r.FilterTerms.Any(term => term.Contains(criteria.SearchString, StringComparison.InvariantCultureIgnoreCase)); {
// Room name isn't translatable, so ToString() is used here for simplicity.
matchingFilter &= r.FilterTerms.Any(term => term.ToString().Contains(criteria.SearchString, StringComparison.InvariantCultureIgnoreCase));
}
r.MatchingFilter = matchingFilter; r.MatchingFilter = matchingFilter;
} }

View File

@ -14,6 +14,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -101,7 +102,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
public bool FilteringActive { get; set; } public bool FilteringActive { get; set; }
public IEnumerable<string> FilterTerms => new[] { Room.Name.Value }; public IEnumerable<LocalisableString> FilterTerms => new LocalisableString[] { Room.Name.Value };
private bool matchingFilter = true; private bool matchingFilter = true;

View File

@ -12,7 +12,6 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics; using osu.Game.Graphics;
@ -416,13 +415,13 @@ namespace osu.Game.Screens.Select
foreach (var mod in mods.Value.OfType<IApplicableToRate>()) foreach (var mod in mods.Value.OfType<IApplicableToRate>())
rate = mod.ApplyToRate(0, rate); rate = mod.ApplyToRate(0, rate);
double bpmMax = beatmap.ControlPointInfo.BPMMaximum * rate; int bpmMax = (int)Math.Round(Math.Round(beatmap.ControlPointInfo.BPMMaximum) * rate);
double bpmMin = beatmap.ControlPointInfo.BPMMinimum * rate; int bpmMin = (int)Math.Round(Math.Round(beatmap.ControlPointInfo.BPMMinimum) * rate);
double mostCommonBPM = 60000 / beatmap.GetMostCommonBeatLength() * rate; int mostCommonBPM = (int)Math.Round(Math.Round(60000 / beatmap.GetMostCommonBeatLength()) * rate);
string labelText = Precision.AlmostEquals(bpmMin, bpmMax) string labelText = bpmMin == bpmMax
? $"{bpmMin:0}" ? $"{bpmMin}"
: $"{bpmMin:0}-{bpmMax:0} (mostly {mostCommonBPM:0})"; : $"{bpmMin}-{bpmMax} (mostly {mostCommonBPM})";
bpmLabelContainer.Child = new InfoLabel(new BeatmapStatistic bpmLabelContainer.Child = new InfoLabel(new BeatmapStatistic
{ {

View File

@ -65,7 +65,7 @@ namespace osu.Game.Stores
{ {
data.Seek(0, SeekOrigin.Begin); data.Seek(0, SeekOrigin.Begin);
using (var output = Storage.GetStream(file.GetStoragePath(), FileAccess.Write)) using (var output = Storage.CreateFileSafely(file.GetStoragePath()))
data.CopyTo(output); data.CopyTo(output);
data.Seek(0, SeekOrigin.Begin); data.Seek(0, SeekOrigin.Begin);

View File

@ -8,6 +8,7 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Logging; using osu.Framework.Logging;
@ -18,6 +19,7 @@ using osu.Game.Database;
using osu.Game.Models; using osu.Game.Models;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Skinning; using osu.Game.Skinning;
using Sentry; using Sentry;
using Sentry.Protocol; using Sentry.Protocol;
@ -109,6 +111,7 @@ namespace osu.Game.Utils
}, scope => }, scope =>
{ {
var beatmap = game.Dependencies.Get<IBindable<WorkingBeatmap>>().Value.BeatmapInfo; var beatmap = game.Dependencies.Get<IBindable<WorkingBeatmap>>().Value.BeatmapInfo;
var ruleset = game.Dependencies.Get<IBindable<RulesetInfo>>().Value;
scope.Contexts[@"config"] = new scope.Contexts[@"config"] = new
{ {
@ -125,6 +128,8 @@ namespace osu.Game.Utils
BeatmapSets = realm.All<BeatmapSetInfo>().Count(), BeatmapSets = realm.All<BeatmapSetInfo>().Count(),
Beatmaps = realm.All<BeatmapInfo>().Count(), Beatmaps = realm.All<BeatmapInfo>().Count(),
Files = realm.All<RealmFile>().Count(), Files = realm.All<RealmFile>().Count(),
Rulesets = realm.All<RulesetInfo>().Count(),
RulesetsAvailable = realm.All<RulesetInfo>().Count(r => r.Available),
Skins = realm.All<SkinInfo>().Count(), Skins = realm.All<SkinInfo>().Count(),
} }
}; };
@ -137,14 +142,27 @@ namespace osu.Game.Utils
scope.Contexts[@"beatmap"] = new scope.Contexts[@"beatmap"] = new
{ {
Name = beatmap.ToString(), Name = beatmap.ToString(),
Ruleset = beatmap.Ruleset.InstantiationInfo,
beatmap.OnlineID, beatmap.OnlineID,
}; };
scope.Contexts[@"ruleset"] = new
{
ruleset.ShortName,
ruleset.Name,
ruleset.InstantiationInfo,
ruleset.OnlineID
};
scope.Contexts[@"clocks"] = new scope.Contexts[@"clocks"] = new
{ {
Audio = game.Dependencies.Get<MusicController>().CurrentTrack.CurrentTime, Audio = game.Dependencies.Get<MusicController>().CurrentTrack.CurrentTime,
Game = game.Clock.CurrentTime, Game = game.Clock.CurrentTime,
}; };
scope.SetTag(@"ruleset", ruleset.ShortName);
scope.SetTag(@"os", $"{RuntimeInfo.OS} ({Environment.OSVersion})");
scope.SetTag(@"processor count", Environment.ProcessorCount.ToString());
}); });
} }
else else

Some files were not shown because too many files have changed in this diff Show More