mirror of
https://github.com/ppy/osu.git
synced 2024-12-13 05:22:54 +08:00
Merge branch 'master' into localisable-filter-items
This commit is contained in:
commit
4633eed78c
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -31,7 +31,7 @@ jobs:
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ github.workspace }}/inspectcode
|
||||
key: inspectcode-${{ hashFiles('.config/dotnet-tools.json', '.github/workflows/ci.yml', 'osu.sln*', '.editorconfig', '.globalconfig') }}
|
||||
key: inspectcode-${{ hashFiles('.config/dotnet-tools.json', '.github/workflows/ci.yml', 'osu.sln*', '.editorconfig', '.globalconfig', 'CodeAnalysis/*') }}
|
||||
|
||||
- name: Dotnet code style
|
||||
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf -p:EnforceCodeStyleInBuild=true
|
||||
|
26
.github/workflows/sentry-release.yml
vendored
Normal file
26
.github/workflows/sentry-release.yml
vendored
Normal 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
2
.gitignore
vendored
@ -340,3 +340,5 @@ inspectcode
|
||||
# Fody (pulled in by Realm) - schema file
|
||||
FodyWeavers.xsd
|
||||
**/FodyWeavers.xml
|
||||
|
||||
.idea/.idea.osu.Desktop/.idea/misc.xml
|
@ -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>
|
@ -11,6 +11,7 @@ T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal exten
|
||||
T:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods.
|
||||
M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast<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.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.
|
||||
|
@ -56,6 +56,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Transitive Dependencies">
|
||||
<!-- 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>
|
||||
</Project>
|
||||
|
@ -18,5 +18,6 @@
|
||||
<file src="**.exe" target="lib\net45\" exclude="**vshost**"/>
|
||||
<file src="**.dll" target="lib\net45\"/>
|
||||
<file src="**.config" target="lib\net45\"/>
|
||||
<file src="**.json" target="lib\net45\"/>
|
||||
</files>
|
||||
</package>
|
||||
|
@ -37,6 +37,8 @@
|
||||
</array>
|
||||
<key>XSAppIconAssets</key>
|
||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
</dict>
|
||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
{
|
||||
public class TestSceneJuiceStreamPlacementBlueprint : CatchPlacementBlueprintTestScene
|
||||
{
|
||||
private const double velocity = 0.5;
|
||||
private const double velocity_factor = 0.5;
|
||||
|
||||
private JuiceStream lastObject => LastObject?.HitObject as JuiceStream;
|
||||
|
||||
@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
{
|
||||
var playable = base.GetPlayableBeatmap();
|
||||
playable.Difficulty.SliderTickRate = 5;
|
||||
playable.Difficulty.SliderMultiplier = velocity * 10;
|
||||
playable.Difficulty.SliderMultiplier = velocity_factor * 10;
|
||||
return playable;
|
||||
}
|
||||
|
||||
@ -43,6 +43,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
AddAssert("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1]));
|
||||
AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0]));
|
||||
AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1]));
|
||||
AddAssert("default slider velocity", () => lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -66,28 +67,21 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVelocityLimit()
|
||||
public void TestSliderVelocityChange()
|
||||
{
|
||||
double[] times = { 100, 300 };
|
||||
float[] positions = { 200, 500 };
|
||||
addPlacementSteps(times, positions);
|
||||
addPathCheckStep(times, new float[] { 200, 300 });
|
||||
}
|
||||
addPathCheckStep(times, positions);
|
||||
|
||||
[Test]
|
||||
public void TestPreviousVerticesAreFixed()
|
||||
{
|
||||
double[] times = { 100, 300, 500, 700 };
|
||||
float[] positions = { 200, 400, 100, 500 };
|
||||
addPlacementSteps(times, positions);
|
||||
addPathCheckStep(times, new float[] { 200, 300, 200, 300 });
|
||||
AddAssert("slider velocity changed", () => !lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClampedPositionIsRestored()
|
||||
{
|
||||
double[] times = { 100, 300, 500 };
|
||||
float[] positions = { 200, 200, 0, 250 };
|
||||
float[] positions = { 200, 200, -3000, 250 };
|
||||
|
||||
addMoveAndClickSteps(times[0], positions[0]);
|
||||
addMoveAndClickSteps(times[1], positions[1]);
|
||||
@ -97,15 +91,6 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
addPathCheckStep(times, new float[] { 200, 200, 250 });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFirstVertexIsFixed()
|
||||
{
|
||||
double[] times = { 100, 200 };
|
||||
float[] positions = { 100, 300 };
|
||||
addPlacementSteps(times, positions);
|
||||
addPathCheckStep(times, new float[] { 100, 150 });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOutOfOrder()
|
||||
{
|
||||
|
@ -101,31 +101,16 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClampedPositionIsRestored()
|
||||
public void TestSliderVelocityChange()
|
||||
{
|
||||
const double velocity = 0.25;
|
||||
double[] times = { 100, 500, 700 };
|
||||
float[] positions = { 100, 100, 100 };
|
||||
addBlueprintStep(times, positions, velocity);
|
||||
double[] times = { 100, 300 };
|
||||
float[] positions = { 200, 300 };
|
||||
addBlueprintStep(times, positions);
|
||||
AddAssert("default slider velocity", () => hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
|
||||
|
||||
addDragStartStep(times[1], positions[1]);
|
||||
|
||||
AddMouseMoveStep(times[1], 200);
|
||||
addVertexCheckStep(3, 1, times[1], 200);
|
||||
addVertexCheckStep(3, 2, times[2], 150);
|
||||
|
||||
AddMouseMoveStep(times[1], 100);
|
||||
addVertexCheckStep(3, 1, times[1], 100);
|
||||
// Stored position is restored.
|
||||
addVertexCheckStep(3, 2, times[2], positions[2]);
|
||||
|
||||
AddMouseMoveStep(times[1], 300);
|
||||
addDragEndStep();
|
||||
addDragStartStep(times[1], 300);
|
||||
|
||||
AddMouseMoveStep(times[1], 100);
|
||||
// Position is different because a changed position is committed when the previous drag is ended.
|
||||
addVertexCheckStep(3, 2, times[2], 250);
|
||||
AddMouseMoveStep(times[1], 400);
|
||||
AddAssert("slider velocity changed", () => !hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -174,7 +159,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
addAddVertexSteps(500, 150);
|
||||
addVertexCheckStep(3, 1, 500, 150);
|
||||
|
||||
addAddVertexSteps(90, 220);
|
||||
addAddVertexSteps(90, 200);
|
||||
addVertexCheckStep(4, 1, times[0], positions[0]);
|
||||
|
||||
addAddVertexSteps(750, 180);
|
||||
@ -234,10 +219,10 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
{
|
||||
var path = new JuiceStreamPath();
|
||||
for (int i = 1; i < times.Length; i++)
|
||||
path.Add((times[i] - times[0]) * velocity, positions[i] - positions[0]);
|
||||
path.Add(times[i] - times[0], positions[i] - positions[0]);
|
||||
|
||||
var sliderPath = new SliderPath();
|
||||
path.ConvertToSliderPath(sliderPath, 0);
|
||||
path.ConvertToSliderPath(sliderPath, 0, velocity);
|
||||
addBlueprintStep(times[0], positions[0], sliderPath, velocity);
|
||||
}
|
||||
|
||||
@ -245,11 +230,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
|
||||
private void addVertexCheckStep(int count, int index, double time, float x) => AddAssert($"vertex {index} of {count} at {time}, {x}", () =>
|
||||
{
|
||||
double expectedDistance = (time - hitObject.StartTime) * hitObject.Velocity;
|
||||
double expectedTime = time - hitObject.StartTime;
|
||||
float expectedX = x - hitObject.OriginalX;
|
||||
var vertices = getVertices();
|
||||
return vertices.Count == count &&
|
||||
Precision.AlmostEquals(vertices[index].Distance, expectedDistance, 1e-3) &&
|
||||
Precision.AlmostEquals(vertices[index].Time, expectedTime, 1e-3) &&
|
||||
Precision.AlmostEquals(vertices[index].X, expectedX);
|
||||
});
|
||||
|
||||
|
@ -5,7 +5,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
@ -37,14 +36,14 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
double distance = rng.NextDouble() * scale * 2 - scale;
|
||||
double time = rng.NextDouble() * scale * 2 - scale;
|
||||
if (integralValues)
|
||||
distance = Math.Round(distance);
|
||||
time = Math.Round(time);
|
||||
|
||||
float oldX = path.PositionAtDistance(distance);
|
||||
int index = path.InsertVertex(distance);
|
||||
float oldX = path.PositionAtTime(time);
|
||||
int index = path.InsertVertex(time);
|
||||
Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount + 1));
|
||||
Assert.That(path.Vertices[index].Distance, Is.EqualTo(distance));
|
||||
Assert.That(path.Vertices[index].Time, Is.EqualTo(time));
|
||||
Assert.That(path.Vertices[index].X, Is.EqualTo(oldX));
|
||||
break;
|
||||
}
|
||||
@ -52,20 +51,20 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
case 1:
|
||||
{
|
||||
int index = rng.Next(path.Vertices.Count);
|
||||
double distance = path.Vertices[index].Distance;
|
||||
double time = path.Vertices[index].Time;
|
||||
float newX = (float)(rng.NextDouble() * scale * 2 - scale);
|
||||
if (integralValues)
|
||||
newX = MathF.Round(newX);
|
||||
|
||||
path.SetVertexPosition(index, newX);
|
||||
Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount));
|
||||
Assert.That(path.Vertices[index].Distance, Is.EqualTo(distance));
|
||||
Assert.That(path.Vertices[index].Time, Is.EqualTo(time));
|
||||
Assert.That(path.Vertices[index].X, Is.EqualTo(newX));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
assertInvariants(path.Vertices, checkSlope);
|
||||
assertInvariants(path.Vertices);
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
path.Add(10, 5);
|
||||
path.Add(20, -5);
|
||||
|
||||
int removeCount = path.RemoveVertices((v, i) => v.Distance == 10 && i == 1);
|
||||
int removeCount = path.RemoveVertices((v, i) => v.Time == 10 && i == 1);
|
||||
Assert.That(removeCount, Is.EqualTo(1));
|
||||
Assert.That(path.Vertices, Is.EqualTo(new[]
|
||||
{
|
||||
@ -131,8 +130,9 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
}));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRandomConvertFromSliderPath()
|
||||
[TestCase(10)]
|
||||
[TestCase(0.1)]
|
||||
public void TestRandomConvertFromSliderPath(double velocity)
|
||||
{
|
||||
var rng = new Random(1);
|
||||
var path = new JuiceStreamPath();
|
||||
@ -162,28 +162,28 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
else
|
||||
sliderPath.ExpectedDistance.Value = null;
|
||||
|
||||
path.ConvertFromSliderPath(sliderPath);
|
||||
Assert.That(path.Vertices[0].Distance, Is.EqualTo(0));
|
||||
Assert.That(path.Distance, Is.EqualTo(sliderPath.Distance).Within(1e-3));
|
||||
assertInvariants(path.Vertices, true);
|
||||
path.ConvertFromSliderPath(sliderPath, velocity);
|
||||
Assert.That(path.Vertices[0].Time, Is.EqualTo(0));
|
||||
Assert.That(path.Duration * velocity, Is.EqualTo(sliderPath.Distance).Within(1e-3));
|
||||
assertInvariants(path.Vertices);
|
||||
|
||||
double[] sampleDistances = Enumerable.Range(0, 10)
|
||||
.Select(_ => rng.NextDouble() * sliderPath.Distance)
|
||||
.ToArray();
|
||||
double[] sampleTimes = Enumerable.Range(0, 10)
|
||||
.Select(_ => rng.NextDouble() * sliderPath.Distance / velocity)
|
||||
.ToArray();
|
||||
|
||||
foreach (double distance in sampleDistances)
|
||||
foreach (double time in sampleTimes)
|
||||
{
|
||||
float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X;
|
||||
Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3));
|
||||
float expected = sliderPath.PositionAt(time * velocity / sliderPath.Distance).X;
|
||||
Assert.That(path.PositionAtTime(time), Is.EqualTo(expected).Within(1e-3));
|
||||
}
|
||||
|
||||
path.ResampleVertices(sampleDistances);
|
||||
assertInvariants(path.Vertices, true);
|
||||
path.ResampleVertices(sampleTimes);
|
||||
assertInvariants(path.Vertices);
|
||||
|
||||
foreach (double distance in sampleDistances)
|
||||
foreach (double time in sampleTimes)
|
||||
{
|
||||
float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X;
|
||||
Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3));
|
||||
float expected = sliderPath.PositionAt(time * velocity / sliderPath.Distance).X;
|
||||
Assert.That(path.PositionAtTime(time), Is.EqualTo(expected).Within(1e-3));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -201,17 +201,17 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
do
|
||||
{
|
||||
double distance = rng.NextDouble() * 1e3;
|
||||
double time = rng.NextDouble() * 1e3;
|
||||
float x = (float)(rng.NextDouble() * 1e3);
|
||||
path.Add(distance, x);
|
||||
path.Add(time, x);
|
||||
} while (rng.Next(5) != 0);
|
||||
|
||||
float sliderStartY = (float)(rng.NextDouble() * JuiceStreamPath.OSU_PLAYFIELD_HEIGHT);
|
||||
|
||||
path.ConvertToSliderPath(sliderPath, sliderStartY);
|
||||
Assert.That(sliderPath.Distance, Is.EqualTo(path.Distance).Within(1e-3));
|
||||
Assert.That(sliderPath.ControlPoints[0].Position.X, Is.EqualTo(path.Vertices[0].X));
|
||||
assertInvariants(path.Vertices, true);
|
||||
double requiredVelocity = path.ComputeRequiredVelocity();
|
||||
double velocity = Math.Clamp(requiredVelocity, 1, 100);
|
||||
|
||||
path.ConvertToSliderPath(sliderPath, sliderStartY, velocity);
|
||||
|
||||
foreach (var point in sliderPath.ControlPoints)
|
||||
{
|
||||
@ -219,11 +219,18 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
Assert.That(sliderStartY + point.Position.Y, Is.InRange(0, JuiceStreamPath.OSU_PLAYFIELD_HEIGHT));
|
||||
}
|
||||
|
||||
Assert.That(sliderPath.ControlPoints[0].Position.X, Is.EqualTo(path.Vertices[0].X));
|
||||
|
||||
// The path is preserved only if required velocity is used.
|
||||
if (velocity < requiredVelocity) continue;
|
||||
|
||||
Assert.That(sliderPath.Distance / velocity, Is.EqualTo(path.Duration).Within(1e-3));
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double distance = rng.NextDouble() * path.Distance;
|
||||
float expected = path.PositionAtDistance(distance);
|
||||
Assert.That(sliderPath.PositionAt(distance / sliderPath.Distance).X, Is.EqualTo(expected).Within(1e-3));
|
||||
double time = rng.NextDouble() * path.Duration;
|
||||
float expected = path.PositionAtTime(time);
|
||||
Assert.That(sliderPath.PositionAt(time * velocity / sliderPath.Distance).X, Is.EqualTo(expected).Within(3e-3));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -244,7 +251,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
path.Add(20, 0);
|
||||
checkNewId();
|
||||
|
||||
path.RemoveVertices((v, _) => v.Distance == 20);
|
||||
path.RemoveVertices((v, _) => v.Time == 20);
|
||||
checkNewId();
|
||||
|
||||
path.ResampleVertices(new double[] { 5, 10, 15 });
|
||||
@ -253,7 +260,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
path.Clear();
|
||||
checkNewId();
|
||||
|
||||
path.ConvertFromSliderPath(new SliderPath());
|
||||
path.ConvertFromSliderPath(new SliderPath(), 1);
|
||||
checkNewId();
|
||||
|
||||
void checkNewId()
|
||||
@ -263,25 +270,19 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
}
|
||||
}
|
||||
|
||||
private void assertInvariants(IReadOnlyList<JuiceStreamPathVertex> vertices, bool checkSlope)
|
||||
private void assertInvariants(IReadOnlyList<JuiceStreamPathVertex> vertices)
|
||||
{
|
||||
Assert.That(vertices, Is.Not.Empty);
|
||||
|
||||
for (int i = 0; i < vertices.Count; i++)
|
||||
{
|
||||
Assert.That(double.IsFinite(vertices[i].Distance));
|
||||
Assert.That(double.IsFinite(vertices[i].Time));
|
||||
Assert.That(float.IsFinite(vertices[i].X));
|
||||
}
|
||||
|
||||
for (int i = 1; i < vertices.Count; i++)
|
||||
{
|
||||
Assert.That(vertices[i].Distance, Is.GreaterThanOrEqualTo(vertices[i - 1].Distance));
|
||||
|
||||
if (!checkSlope) continue;
|
||||
|
||||
float xDiff = Math.Abs(vertices[i].X - vertices[i - 1].X);
|
||||
double distanceDiff = vertices[i].Distance - vertices[i - 1].Distance;
|
||||
Assert.That(xDiff, Is.LessThanOrEqualTo(distanceDiff).Within(Precision.FLOAT_EPSILON));
|
||||
Assert.That(vertices[i].Time, Is.GreaterThanOrEqualTo(vertices[i - 1].Time));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
|
||||
public int VertexCount => path.Vertices.Count;
|
||||
|
||||
protected readonly Func<float, double> PositionToDistance;
|
||||
protected readonly Func<float, double> PositionToTime;
|
||||
|
||||
protected IReadOnlyList<VertexState> VertexStates => vertexStates;
|
||||
|
||||
@ -44,9 +44,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
[CanBeNull]
|
||||
private IBeatSnapProvider beatSnapProvider { get; set; }
|
||||
|
||||
protected EditablePath(Func<float, double> positionToDistance)
|
||||
protected EditablePath(Func<float, double> positionToTime)
|
||||
{
|
||||
PositionToDistance = positionToDistance;
|
||||
PositionToTime = positionToTime;
|
||||
|
||||
Anchor = Anchor.BottomLeft;
|
||||
}
|
||||
@ -59,13 +59,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
while (InternalChildren.Count < path.Vertices.Count)
|
||||
AddInternal(new VertexPiece());
|
||||
|
||||
double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity);
|
||||
double timeToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1);
|
||||
|
||||
for (int i = 0; i < VertexCount; i++)
|
||||
{
|
||||
var piece = (VertexPiece)InternalChildren[i];
|
||||
var vertex = path.Vertices[i];
|
||||
piece.Position = new Vector2(vertex.X, (float)(vertex.Distance * distanceToYFactor));
|
||||
piece.Position = new Vector2(vertex.X, (float)(vertex.Time * timeToYFactor));
|
||||
piece.UpdateFrom(vertexStates[i]);
|
||||
}
|
||||
}
|
||||
@ -73,14 +73,14 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
public void InitializeFromHitObject(JuiceStream hitObject)
|
||||
{
|
||||
var sliderPath = hitObject.Path;
|
||||
path.ConvertFromSliderPath(sliderPath);
|
||||
path.ConvertFromSliderPath(sliderPath, hitObject.Velocity);
|
||||
|
||||
// If the original slider path has non-linear type segments, resample the vertices at nested hit object times to reduce the number of vertices.
|
||||
if (sliderPath.ControlPoints.Any(p => p.Type != null && p.Type != PathType.Linear))
|
||||
{
|
||||
path.ResampleVertices(hitObject.NestedHitObjects
|
||||
.Skip(1).TakeWhile(h => !(h is Fruit)) // Only droplets in the first span are used.
|
||||
.Select(h => (h.StartTime - hitObject.StartTime) * hitObject.Velocity));
|
||||
.Select(h => h.StartTime - hitObject.StartTime));
|
||||
}
|
||||
|
||||
vertexStates.Clear();
|
||||
@ -92,11 +92,26 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
|
||||
public void UpdateHitObjectFromPath(JuiceStream hitObject)
|
||||
{
|
||||
path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY);
|
||||
// The SV setting may need to be changed for the current path.
|
||||
var svBindable = hitObject.DifficultyControlPoint.SliderVelocityBindable;
|
||||
double svToVelocityFactor = hitObject.Velocity / svBindable.Value;
|
||||
double requiredVelocity = path.ComputeRequiredVelocity();
|
||||
|
||||
// The value is pre-rounded here because setting it to the bindable will rounded to the nearest value
|
||||
// but it should be always rounded up to satisfy the required minimum velocity condition.
|
||||
//
|
||||
// This is rounded to integers instead of using the precision of the bindable
|
||||
// because it results in a smaller number of non-redundant control points.
|
||||
//
|
||||
// The value is clamped here by the bindable min and max values.
|
||||
// In case the required velocity is too large, the path is not preserved.
|
||||
svBindable.Value = Math.Ceiling(requiredVelocity / svToVelocityFactor);
|
||||
|
||||
path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY, hitObject.Velocity);
|
||||
|
||||
if (beatSnapProvider == null) return;
|
||||
|
||||
double endTime = hitObject.StartTime + path.Distance / hitObject.Velocity;
|
||||
double endTime = hitObject.StartTime + path.Duration;
|
||||
double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime);
|
||||
hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity;
|
||||
}
|
||||
@ -108,9 +123,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
|
||||
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
|
||||
|
||||
protected int AddVertex(double distance, float x)
|
||||
protected int AddVertex(double time, float x)
|
||||
{
|
||||
int index = path.InsertVertex(distance);
|
||||
int index = path.InsertVertex(time);
|
||||
path.SetVertexPosition(index, x);
|
||||
vertexStates.Insert(index, new VertexState());
|
||||
|
||||
@ -138,9 +153,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void MoveSelectedVertices(double distanceDelta, float xDelta)
|
||||
protected void MoveSelectedVertices(double timeDelta, float xDelta)
|
||||
{
|
||||
// Because the vertex list may be reordered due to distance change, the state list must be reordered as well.
|
||||
// Because the vertex list may be reordered due to time change, the state list must be reordered as well.
|
||||
previousVertexStates.Clear();
|
||||
previousVertexStates.AddRange(vertexStates);
|
||||
|
||||
@ -152,11 +167,11 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
for (int i = 1; i < vertexCount; i++)
|
||||
{
|
||||
var state = previousVertexStates[i];
|
||||
double distance = state.VertexBeforeChange.Distance;
|
||||
double time = state.VertexBeforeChange.Time;
|
||||
if (state.IsSelected)
|
||||
distance += distanceDelta;
|
||||
time += timeDelta;
|
||||
|
||||
int newIndex = path.InsertVertex(Math.Max(0, distance));
|
||||
int newIndex = path.InsertVertex(Math.Max(0, time));
|
||||
vertexStates.Insert(newIndex, state);
|
||||
}
|
||||
|
||||
|
@ -15,15 +15,15 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
/// </summary>
|
||||
private JuiceStreamPathVertex lastVertex;
|
||||
|
||||
public PlacementEditablePath(Func<float, double> positionToDistance)
|
||||
: base(positionToDistance)
|
||||
public PlacementEditablePath(Func<float, double> positionToTime)
|
||||
: base(positionToTime)
|
||||
{
|
||||
}
|
||||
|
||||
public void AddNewVertex()
|
||||
{
|
||||
var endVertex = Vertices[^1];
|
||||
int index = AddVertex(endVertex.Distance, endVertex.X);
|
||||
int index = AddVertex(endVertex.Time, endVertex.X);
|
||||
|
||||
for (int i = 0; i < VertexCount; i++)
|
||||
{
|
||||
@ -41,9 +41,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
public void MoveLastVertex(Vector2 screenSpacePosition)
|
||||
{
|
||||
Vector2 position = ToRelativePosition(screenSpacePosition);
|
||||
double distanceDelta = PositionToDistance(position.Y) - lastVertex.Distance;
|
||||
double timeDelta = PositionToTime(position.Y) - lastVertex.Time;
|
||||
float xDelta = position.X - lastVertex.X;
|
||||
MoveSelectedVertices(distanceDelta, xDelta);
|
||||
MoveSelectedVertices(timeDelta, xDelta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
{
|
||||
private readonly Path drawablePath;
|
||||
|
||||
private readonly List<(double Distance, float X)> vertices = new List<(double, float)>();
|
||||
private readonly List<(double Time, float X)> vertices = new List<(double, float)>();
|
||||
|
||||
public ScrollingPath()
|
||||
{
|
||||
@ -35,16 +35,16 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
|
||||
public void UpdatePathFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject)
|
||||
{
|
||||
double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity);
|
||||
double timeToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1);
|
||||
|
||||
computeDistanceXs(hitObject);
|
||||
computeTimeXs(hitObject);
|
||||
drawablePath.Vertices = vertices
|
||||
.Select(v => new Vector2(v.X, (float)(v.Distance * distanceToYFactor)))
|
||||
.Select(v => new Vector2(v.X, (float)(v.Time * timeToYFactor)))
|
||||
.ToArray();
|
||||
drawablePath.OriginPosition = drawablePath.PositionInBoundingBox(Vector2.Zero);
|
||||
}
|
||||
|
||||
private void computeDistanceXs(JuiceStream hitObject)
|
||||
private void computeTimeXs(JuiceStream hitObject)
|
||||
{
|
||||
vertices.Clear();
|
||||
|
||||
@ -54,17 +54,17 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
if (sliderVertices.Count == 0)
|
||||
return;
|
||||
|
||||
double distance = 0;
|
||||
double time = 0;
|
||||
Vector2 lastPosition = Vector2.Zero;
|
||||
|
||||
for (int repeat = 0; repeat < hitObject.RepeatCount + 1; repeat++)
|
||||
{
|
||||
foreach (var position in sliderVertices)
|
||||
{
|
||||
distance += Vector2.Distance(lastPosition, position);
|
||||
time += Vector2.Distance(lastPosition, position) / hitObject.Velocity;
|
||||
lastPosition = position;
|
||||
|
||||
vertices.Add((distance, position.X));
|
||||
vertices.Add((time, position.X));
|
||||
}
|
||||
|
||||
sliderVertices.Reverse();
|
||||
|
@ -27,15 +27,15 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
[CanBeNull]
|
||||
private IEditorChangeHandler changeHandler { get; set; }
|
||||
|
||||
public SelectionEditablePath(Func<float, double> positionToDistance)
|
||||
: base(positionToDistance)
|
||||
public SelectionEditablePath(Func<float, double> positionToTime)
|
||||
: base(positionToTime)
|
||||
{
|
||||
}
|
||||
|
||||
public void AddVertex(Vector2 relativePosition)
|
||||
{
|
||||
double distance = Math.Max(0, PositionToDistance(relativePosition.Y));
|
||||
int index = AddVertex(distance, relativePosition.X);
|
||||
double time = Math.Max(0, PositionToTime(relativePosition.Y));
|
||||
int index = AddVertex(time, relativePosition.X);
|
||||
selectOnly(index);
|
||||
}
|
||||
|
||||
@ -83,9 +83,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
protected override void OnDrag(DragEvent e)
|
||||
{
|
||||
Vector2 mousePosition = ToRelativePosition(e.ScreenSpaceMousePosition);
|
||||
double distanceDelta = PositionToDistance(mousePosition.Y) - PositionToDistance(dragStartPosition.Y);
|
||||
double timeDelta = PositionToTime(mousePosition.Y) - PositionToTime(dragStartPosition.Y);
|
||||
float xDelta = mousePosition.X - dragStartPosition.X;
|
||||
MoveSelectedVertices(distanceDelta, xDelta);
|
||||
MoveSelectedVertices(timeDelta, xDelta);
|
||||
}
|
||||
|
||||
protected override void OnDragEnd(DragEndEvent e)
|
||||
|
@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
scrollingPath = new ScrollingPath(),
|
||||
nestedOutlineContainer = new NestedOutlineContainer(),
|
||||
editablePath = new PlacementEditablePath(positionToDistance)
|
||||
editablePath = new PlacementEditablePath(positionToTime)
|
||||
};
|
||||
}
|
||||
|
||||
@ -121,10 +121,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
lastEditablePathId = editablePath.PathId;
|
||||
}
|
||||
|
||||
private double positionToDistance(float relativeYPosition)
|
||||
private double positionToTime(float relativeYPosition)
|
||||
{
|
||||
double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime);
|
||||
return (time - HitObject.StartTime) * HitObject.Velocity;
|
||||
return time - HitObject.StartTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
scrollingPath = new ScrollingPath(),
|
||||
nestedOutlineContainer = new NestedOutlineContainer(),
|
||||
editablePath = new SelectionEditablePath(positionToDistance)
|
||||
editablePath = new SelectionEditablePath(positionToTime)
|
||||
};
|
||||
}
|
||||
|
||||
@ -145,10 +145,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
return new RectangleF(left, top, right - left, bottom - top).Inflate(objectRadius);
|
||||
}
|
||||
|
||||
private double positionToDistance(float relativeYPosition)
|
||||
private double positionToTime(float relativeYPosition)
|
||||
{
|
||||
double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime);
|
||||
return (time - HitObject.StartTime) * HitObject.Velocity;
|
||||
return time - HitObject.StartTime;
|
||||
}
|
||||
|
||||
private void initializeJuiceStreamPath()
|
||||
|
@ -6,6 +6,7 @@ using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input;
|
||||
@ -89,15 +90,19 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler })
|
||||
});
|
||||
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||
{
|
||||
var result = base.FindSnappedPositionAndTime(screenSpacePosition);
|
||||
var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
|
||||
|
||||
result.ScreenSpacePosition.X = screenSpacePosition.X;
|
||||
|
||||
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
|
||||
Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius)
|
||||
if (snapType.HasFlagFast(SnapType.Grids))
|
||||
{
|
||||
result = snapResult;
|
||||
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
|
||||
Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius)
|
||||
{
|
||||
result = snapResult;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
@ -27,10 +27,16 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
public int RepeatCount { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public double Velocity { get; private set; }
|
||||
private double velocityFactor;
|
||||
|
||||
[JsonIgnore]
|
||||
public double TickDistance { get; private set; }
|
||||
private double tickDistanceFactor;
|
||||
|
||||
[JsonIgnore]
|
||||
public double Velocity => velocityFactor * DifficultyControlPoint.SliderVelocity;
|
||||
|
||||
[JsonIgnore]
|
||||
public double TickDistance => tickDistanceFactor * DifficultyControlPoint.SliderVelocity;
|
||||
|
||||
/// <summary>
|
||||
/// 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);
|
||||
|
||||
double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
|
||||
|
||||
Velocity = scoringDistance / timingPoint.BeatLength;
|
||||
TickDistance = scoringDistance / difficulty.SliderTickRate;
|
||||
velocityFactor = base_scoring_distance * difficulty.SliderMultiplier / timingPoint.BeatLength;
|
||||
tickDistanceFactor = base_scoring_distance * difficulty.SliderMultiplier / difficulty.SliderTickRate;
|
||||
}
|
||||
|
||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||
|
@ -20,11 +20,6 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
/// 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.
|
||||
/// </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>
|
||||
public class JuiceStreamPath
|
||||
{
|
||||
@ -46,9 +41,9 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
public int InvalidationID { get; private set; } = 1;
|
||||
|
||||
/// <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>
|
||||
public double Distance => vertices[^1].Distance - vertices[0].Distance;
|
||||
public double Duration => vertices[^1].Time - vertices[0].Time;
|
||||
|
||||
/// <remarks>
|
||||
/// This list should always be non-empty.
|
||||
@ -59,15 +54,15 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
};
|
||||
|
||||
/// <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>
|
||||
/// <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>
|
||||
public float PositionAtDistance(double distance)
|
||||
public float PositionAtTime(double time)
|
||||
{
|
||||
int index = vertexIndexAtDistance(distance);
|
||||
return positionAtDistance(distance, index);
|
||||
int index = vertexIndexAtTime(time);
|
||||
return positionAtTime(time, index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -81,19 +76,19 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Insert a vertex at given <paramref name="distance"/>.
|
||||
/// The <see cref="PositionAtDistance"/> is used as the position of the new vertex.
|
||||
/// Insert a vertex at given <paramref name="time"/>.
|
||||
/// 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).
|
||||
/// </summary>
|
||||
/// <returns>The index of the new vertex.</returns>
|
||||
public int InsertVertex(double distance)
|
||||
public int InsertVertex(double time)
|
||||
{
|
||||
if (!double.IsFinite(distance))
|
||||
throw new ArgumentOutOfRangeException(nameof(distance));
|
||||
if (!double.IsFinite(time))
|
||||
throw new ArgumentOutOfRangeException(nameof(time));
|
||||
|
||||
int index = vertexIndexAtDistance(distance);
|
||||
float x = positionAtDistance(distance, index);
|
||||
vertices.Insert(index, new JuiceStreamPathVertex(distance, x));
|
||||
int index = vertexIndexAtTime(time);
|
||||
float x = positionAtTime(time, index);
|
||||
vertices.Insert(index, new JuiceStreamPathVertex(time, x));
|
||||
|
||||
invalidate();
|
||||
return index;
|
||||
@ -101,7 +96,6 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
public void SetVertexPosition(int index, float newX)
|
||||
{
|
||||
@ -111,32 +105,17 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
if (!float.IsFinite(newX))
|
||||
throw new ArgumentOutOfRangeException(nameof(newX));
|
||||
|
||||
var newVertex = new JuiceStreamPathVertex(vertices[index].Distance, newX);
|
||||
|
||||
for (int i = index - 1; i >= 0 && !canConnect(vertices[i], newVertex); i--)
|
||||
{
|
||||
float clampedX = clampToConnectablePosition(newVertex, vertices[i]);
|
||||
vertices[i] = new JuiceStreamPathVertex(vertices[i].Distance, clampedX);
|
||||
}
|
||||
|
||||
for (int i = index + 1; i < vertices.Count; i++)
|
||||
{
|
||||
float clampedX = clampToConnectablePosition(newVertex, vertices[i]);
|
||||
vertices[i] = new JuiceStreamPathVertex(vertices[i].Distance, clampedX);
|
||||
}
|
||||
|
||||
vertices[index] = newVertex;
|
||||
vertices[index] = new JuiceStreamPathVertex(vertices[index].Time, newX);
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a new vertex at given <paramref name="distance"/> and position.
|
||||
/// Adjacent vertices are moved when necessary in the same way as <see cref="SetVertexPosition"/>.
|
||||
/// Add a new vertex at given <paramref name="time"/> and position.
|
||||
/// </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);
|
||||
}
|
||||
|
||||
@ -163,22 +142,22 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recreate this path by using difference set of vertices at given distances.
|
||||
/// In addition to the given <paramref name="sampleDistances"/>, 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.
|
||||
/// Recreate this path by using difference set of vertices at given time points.
|
||||
/// 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="PositionAtTime"/>s at <paramref name="sampleTimes"/> are preserved.
|
||||
/// </summary>
|
||||
public void ResampleVertices(IEnumerable<double> sampleDistances)
|
||||
public void ResampleVertices(IEnumerable<double> sampleTimes)
|
||||
{
|
||||
var sampledVertices = new List<JuiceStreamPathVertex>();
|
||||
|
||||
foreach (double distance in sampleDistances)
|
||||
foreach (double time in sampleTimes)
|
||||
{
|
||||
if (!double.IsFinite(distance))
|
||||
throw new ArgumentOutOfRangeException(nameof(sampleDistances));
|
||||
if (!double.IsFinite(time))
|
||||
throw new ArgumentOutOfRangeException(nameof(sampleTimes));
|
||||
|
||||
double clampedDistance = Math.Clamp(distance, vertices[0].Distance, vertices[^1].Distance);
|
||||
float x = PositionAtDistance(clampedDistance);
|
||||
sampledVertices.Add(new JuiceStreamPathVertex(clampedDistance, x));
|
||||
double clampedTime = Math.Clamp(time, vertices[0].Time, vertices[^1].Time);
|
||||
float x = PositionAtTime(clampedTime);
|
||||
sampledVertices.Add(new JuiceStreamPathVertex(clampedTime, x));
|
||||
}
|
||||
|
||||
sampledVertices.Sort();
|
||||
@ -196,37 +175,62 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
/// <remarks>
|
||||
/// Duplicated vertices are automatically removed.
|
||||
/// </remarks>
|
||||
public void ConvertFromSliderPath(SliderPath sliderPath)
|
||||
public void ConvertFromSliderPath(SliderPath sliderPath, double velocity)
|
||||
{
|
||||
var sliderPathVertices = new List<Vector2>();
|
||||
sliderPath.GetPathToProgress(sliderPathVertices, 0, 1);
|
||||
|
||||
double distance = 0;
|
||||
double time = 0;
|
||||
|
||||
vertices.Clear();
|
||||
vertices.Add(new JuiceStreamPathVertex(0, sliderPathVertices.FirstOrDefault().X));
|
||||
|
||||
for (int i = 1; i < sliderPathVertices.Count; i++)
|
||||
{
|
||||
distance += Vector2.Distance(sliderPathVertices[i - 1], sliderPathVertices[i]);
|
||||
time += Vector2.Distance(sliderPathVertices[i - 1], sliderPathVertices[i]) / velocity;
|
||||
|
||||
if (!Precision.AlmostEquals(vertices[^1].Distance, distance))
|
||||
vertices.Add(new JuiceStreamPathVertex(distance, sliderPathVertices[i].X));
|
||||
if (!Precision.AlmostEquals(vertices[^1].Time, time))
|
||||
Add(time, sliderPathVertices[i].X);
|
||||
}
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// 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 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>
|
||||
public void ConvertToSliderPath(SliderPath sliderPath, float sliderStartY)
|
||||
public void ConvertToSliderPath(SliderPath sliderPath, float sliderStartY, double velocity)
|
||||
{
|
||||
const float margin = 1;
|
||||
|
||||
// Note: these two variables and `sliderPath` are modified by the local functions.
|
||||
double currentDistance = 0;
|
||||
double currentTime = 0;
|
||||
Vector2 lastPosition = new Vector2(vertices[0].X, 0);
|
||||
|
||||
sliderPath.ControlPoints.Clear();
|
||||
@ -237,10 +241,10 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
sliderPath.ControlPoints[^1].Type = PathType.Linear;
|
||||
|
||||
float deltaX = vertices[i].X - lastPosition.X;
|
||||
double length = vertices[i].Distance - currentDistance;
|
||||
double length = (vertices[i].Time - currentTime) * velocity;
|
||||
|
||||
// Should satisfy `deltaX^2 + deltaY^2 = length^2`.
|
||||
// By invariants, the expression inside the `sqrt` is (almost) non-negative.
|
||||
// The expression inside the `sqrt` is (almost) non-negative if the slider velocity is large enough.
|
||||
double deltaY = Math.Sqrt(Math.Max(0, length * length - (double)deltaX * deltaX));
|
||||
|
||||
// When `deltaY` is small, one segment is always enough.
|
||||
@ -280,59 +284,38 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
{
|
||||
Vector2 nextPosition = new Vector2(nextX, nextY);
|
||||
sliderPath.ControlPoints.Add(new PathControlPoint(nextPosition));
|
||||
currentDistance += Vector2.Distance(lastPosition, nextPosition);
|
||||
currentTime += Vector2.Distance(lastPosition, nextPosition) / velocity;
|
||||
lastPosition = nextPosition;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
private int vertexIndexAtDistance(double distance)
|
||||
private int vertexIndexAtTime(double time)
|
||||
{
|
||||
// The position of `(distance, Infinity)` is uniquely determined because infinite positions are not allowed.
|
||||
int i = vertices.BinarySearch(new JuiceStreamPathVertex(distance, float.PositiveInfinity));
|
||||
// The position of `(time, Infinity)` is uniquely determined because infinite positions are not allowed.
|
||||
int i = vertices.BinarySearch(new JuiceStreamPathVertex(time, float.PositiveInfinity));
|
||||
return i < 0 ? ~i : i;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
private float positionAtDistance(double distance, int index)
|
||||
private float positionAtTime(double time, int index)
|
||||
{
|
||||
if (index <= 0)
|
||||
return vertices[0].X;
|
||||
if (index >= vertices.Count)
|
||||
return vertices[^1].X;
|
||||
|
||||
double length = vertices[index].Distance - vertices[index - 1].Distance;
|
||||
if (Precision.AlmostEquals(length, 0))
|
||||
double duration = vertices[index].Time - vertices[index - 1].Time;
|
||||
if (Precision.AlmostEquals(duration, 0))
|
||||
return vertices[index].X;
|
||||
|
||||
float deltaX = vertices[index].X - vertices[index - 1].X;
|
||||
|
||||
return (float)(vertices[index - 1].X + deltaX * ((distance - vertices[index - 1].Distance) / length));
|
||||
}
|
||||
|
||||
/// <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);
|
||||
return (float)(vertices[index - 1].X + deltaX * ((time - vertices[index - 1].Time) / duration));
|
||||
}
|
||||
|
||||
private void invalidate() => InvalidationID++;
|
||||
|
@ -12,22 +12,22 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
/// </summary>
|
||||
public readonly struct JuiceStreamPathVertex : IComparable<JuiceStreamPathVertex>
|
||||
{
|
||||
public readonly double Distance;
|
||||
public readonly double Time;
|
||||
|
||||
public readonly float X;
|
||||
|
||||
public JuiceStreamPathVertex(double distance, float x)
|
||||
public JuiceStreamPathVertex(double time, float x)
|
||||
{
|
||||
Distance = distance;
|
||||
Time = time;
|
||||
X = x;
|
||||
}
|
||||
|
||||
public int CompareTo(JuiceStreamPathVertex other)
|
||||
{
|
||||
int c = Distance.CompareTo(other.Distance);
|
||||
int c = Time.CompareTo(other.Time);
|
||||
return c != 0 ? c : X.CompareTo(other.X);
|
||||
}
|
||||
|
||||
public override string ToString() => $"({Distance}, {X})";
|
||||
public override string ToString() => $"({Time}, {X})";
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,8 @@
|
||||
</array>
|
||||
<key>XSAppIconAssets</key>
|
||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
</dict>
|
||||
|
@ -97,12 +97,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
set => InternalChild = value;
|
||||
}
|
||||
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public override SnapResult FindSnappedPosition(Vector2 screenSpacePosition)
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
@ -5,7 +5,10 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Skinning.Default;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
@ -52,8 +55,29 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
{
|
||||
base.UpdateTimeAndPosition(result);
|
||||
|
||||
if (PlacementActive == PlacementState.Waiting)
|
||||
Column = result.Playfield as Column;
|
||||
if (result.Playfield is Column col)
|
||||
{
|
||||
// Apply an offset to better align with the visual grid.
|
||||
// This should only be applied during placement, as during selection / drag operations the movement is relative
|
||||
// to the initial point of interaction rather than the grid.
|
||||
switch (col.ScrollingInfo.Direction.Value)
|
||||
{
|
||||
case ScrollingDirection.Down:
|
||||
result.ScreenSpacePosition -= new Vector2(0, getNoteHeight(col) / 2);
|
||||
break;
|
||||
|
||||
case ScrollingDirection.Up:
|
||||
result.ScreenSpacePosition += new Vector2(0, getNoteHeight(col) / 2);
|
||||
break;
|
||||
}
|
||||
|
||||
if (PlacementActive == PlacementState.Waiting)
|
||||
Column = col;
|
||||
}
|
||||
}
|
||||
|
||||
private float getNoteHeight(Column resultPlayfield) =>
|
||||
resultPlayfield.ToScreenSpace(new Vector2(DefaultNotePiece.NOTE_HEIGHT)).Y -
|
||||
resultPlayfield.ToScreenSpace(Vector2.Zero).Y;
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,14 @@
|
||||
// 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.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Input;
|
||||
using osu.Game.Rulesets.Mania.Skinning.Default;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@ -56,28 +55,6 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
protected override Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) =>
|
||||
Playfield.GetColumnByPosition(screenSpacePosition);
|
||||
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
|
||||
{
|
||||
var result = base.FindSnappedPositionAndTime(screenSpacePosition);
|
||||
|
||||
switch (ScrollingInfo.Direction.Value)
|
||||
{
|
||||
case ScrollingDirection.Down:
|
||||
result.ScreenSpacePosition -= new Vector2(0, getNoteHeight() / 2);
|
||||
break;
|
||||
|
||||
case ScrollingDirection.Up:
|
||||
result.ScreenSpacePosition += new Vector2(0, getNoteHeight() / 2);
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private float getNoteHeight() =>
|
||||
Playfield.GetColumn(0).ToScreenSpace(new Vector2(DefaultNotePiece.NOTE_HEIGHT)).Y -
|
||||
Playfield.GetColumn(0).ToScreenSpace(Vector2.Zero).Y;
|
||||
|
||||
protected override DrawableRuleset<ManiaHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
||||
{
|
||||
drawableRuleset = new DrawableManiaEditorRuleset(ruleset, beatmap, mods);
|
||||
|
@ -37,6 +37,8 @@
|
||||
</array>
|
||||
<key>XSAppIconAssets</key>
|
||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
</dict>
|
||||
|
@ -7,6 +7,7 @@ using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
@ -123,33 +124,27 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
}
|
||||
}
|
||||
|
||||
public override SnapResult FindSnappedPosition(Vector2 screenSpacePosition)
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||
{
|
||||
if (snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
|
||||
if (snapType.HasFlagFast(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
|
||||
return snapResult;
|
||||
|
||||
return new SnapResult(screenSpacePosition, null);
|
||||
}
|
||||
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
|
||||
{
|
||||
var positionSnap = FindSnappedPosition(screenSpacePosition);
|
||||
if (positionSnap.ScreenSpacePosition != screenSpacePosition)
|
||||
return positionSnap;
|
||||
|
||||
if (distanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
|
||||
if (snapType.HasFlagFast(SnapType.Grids))
|
||||
{
|
||||
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
|
||||
return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition));
|
||||
if (distanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
|
||||
{
|
||||
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
|
||||
return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition));
|
||||
}
|
||||
|
||||
if (rectangularGridSnapToggle.Value == TernaryState.True)
|
||||
{
|
||||
Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(screenSpacePosition));
|
||||
return new SnapResult(rectangularPositionSnapGrid.ToScreenSpace(pos), null, PlayfieldAtScreenSpacePosition(screenSpacePosition));
|
||||
}
|
||||
}
|
||||
|
||||
if (rectangularGridSnapToggle.Value == TernaryState.True)
|
||||
{
|
||||
Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(screenSpacePosition));
|
||||
return new SnapResult(rectangularPositionSnapGrid.ToScreenSpace(pos), null, PlayfieldAtScreenSpacePosition(screenSpacePosition));
|
||||
}
|
||||
|
||||
return base.FindSnappedPositionAndTime(screenSpacePosition);
|
||||
return base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
|
||||
}
|
||||
|
||||
private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult)
|
||||
|
@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
private readonly Bindable<Color4> accentColour = new Bindable<Color4>();
|
||||
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; }
|
||||
|
||||
[Resolved]
|
||||
|
@ -37,6 +37,8 @@
|
||||
</array>
|
||||
<key>XSAppIconAssets</key>
|
||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
</dict>
|
||||
|
@ -37,6 +37,8 @@
|
||||
</array>
|
||||
<key>XSAppIconAssets</key>
|
||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
</dict>
|
||||
|
@ -863,5 +863,40 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
Assert.That(decoded.Difficulty.OverallDifficulty, Is.EqualTo(1));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLegacyAdjacentImplicitCatmullSegmentsAreMerged()
|
||||
{
|
||||
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
|
||||
|
||||
using (var resStream = TestResources.OpenResource("adjacent-catmull-segments.osu"))
|
||||
using (var stream = new LineBufferedReader(resStream))
|
||||
{
|
||||
var decoded = decoder.Decode(stream);
|
||||
var controlPoints = ((IHasPath)decoded.HitObjects[0]).Path.ControlPoints;
|
||||
|
||||
Assert.That(controlPoints.Count, Is.EqualTo(6));
|
||||
Assert.That(controlPoints.Single(c => c.Type != null).Type, Is.EqualTo(PathType.Catmull));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNonLegacyAdjacentImplicitCatmullSegmentsAreNotMerged()
|
||||
{
|
||||
var decoder = new LegacyBeatmapDecoder(LegacyBeatmapEncoder.FIRST_LAZER_VERSION) { ApplyOffsets = false };
|
||||
|
||||
using (var resStream = TestResources.OpenResource("adjacent-catmull-segments.osu"))
|
||||
using (var stream = new LineBufferedReader(resStream))
|
||||
{
|
||||
var decoded = decoder.Decode(stream);
|
||||
var controlPoints = ((IHasPath)decoded.HitObjects[0]).Path.ControlPoints;
|
||||
|
||||
Assert.That(controlPoints.Count, Is.EqualTo(4));
|
||||
Assert.That(controlPoints[0].Type, Is.EqualTo(PathType.Catmull));
|
||||
Assert.That(controlPoints[1].Type, Is.EqualTo(PathType.Catmull));
|
||||
Assert.That(controlPoints[2].Type, Is.EqualTo(PathType.Catmull));
|
||||
Assert.That(controlPoints[3].Type, Is.Null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
2
osu.Game.Tests/Resources/adjacent-catmull-segments.osu
Normal file
2
osu.Game.Tests/Resources/adjacent-catmull-segments.osu
Normal 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
|
@ -185,10 +185,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
private class SnapProvider : IDistanceSnapProvider
|
||||
{
|
||||
public SnapResult FindSnappedPosition(Vector2 screenSpacePosition) =>
|
||||
new SnapResult(screenSpacePosition, null);
|
||||
|
||||
public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
|
||||
public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.Grids) => new SnapResult(screenSpacePosition, 0);
|
||||
|
||||
public Bindable<double> DistanceSpacingMultiplier { get; } = new BindableDouble(1);
|
||||
|
||||
|
@ -5,6 +5,8 @@ using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using JetBrains.Annotations;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@ -31,7 +33,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
[TestFixture]
|
||||
public class TestSceneChatOverlayV2 : OsuManualInputManagerTestScene
|
||||
{
|
||||
private ChatOverlayV2 chatOverlay;
|
||||
private TestChatOverlayV2 chatOverlay;
|
||||
private ChannelManager channelManager;
|
||||
|
||||
private APIUser testUser;
|
||||
@ -61,7 +63,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
Children = new Drawable[]
|
||||
{
|
||||
channelManager,
|
||||
chatOverlay = new ChatOverlayV2 { RelativeSizeAxes = Axes.Both },
|
||||
chatOverlay = new TestChatOverlayV2 { RelativeSizeAxes = Axes.Both },
|
||||
},
|
||||
};
|
||||
});
|
||||
@ -365,19 +367,19 @@ namespace osu.Game.Tests.Visual.Online
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TextBoxRetainsFocus()
|
||||
public void TestTextBoxRetainsFocus()
|
||||
{
|
||||
AddStep("Show overlay", () => chatOverlay.Show());
|
||||
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
||||
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
|
||||
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
|
||||
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
||||
AddStep("Click drawable channel", () => clickDrawable(currentDrawableChannel));
|
||||
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
||||
AddStep("Click selector", () => clickDrawable(chatOverlay.ChildrenOfType<ChannelListSelector>().Single()));
|
||||
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
||||
AddStep("Click listing", () => clickDrawable(chatOverlay.ChildrenOfType<ChannelListing>().Single()));
|
||||
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
||||
AddStep("Click drawable channel", () => clickDrawable(currentDrawableChannel));
|
||||
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
||||
AddStep("Click channel list", () => clickDrawable(chatOverlay.ChildrenOfType<ChannelList>().Single()));
|
||||
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
||||
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);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSlowLoadingChannel()
|
||||
{
|
||||
AddStep("Show overlay (slow-loading)", () =>
|
||||
{
|
||||
chatOverlay.Show();
|
||||
chatOverlay.SlowLoading = true;
|
||||
});
|
||||
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
|
||||
AddAssert("Channel 1 loading", () => !channelIsVisible && chatOverlay.GetSlowLoadingChannel(testChannel1).LoadState == LoadState.Loading);
|
||||
|
||||
AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2));
|
||||
AddStep("Select channel 2", () => clickDrawable(getChannelListItem(testChannel2)));
|
||||
AddAssert("Channel 2 loading", () => !channelIsVisible && chatOverlay.GetSlowLoadingChannel(testChannel2).LoadState == LoadState.Loading);
|
||||
|
||||
AddStep("Finish channel 1 load", () => chatOverlay.GetSlowLoadingChannel(testChannel1).LoadEvent.Set());
|
||||
AddAssert("Channel 1 ready", () => chatOverlay.GetSlowLoadingChannel(testChannel1).LoadState == LoadState.Ready);
|
||||
AddAssert("Channel 1 not displayed", () => !channelIsVisible);
|
||||
|
||||
AddStep("Finish channel 2 load", () => chatOverlay.GetSlowLoadingChannel(testChannel2).LoadEvent.Set());
|
||||
AddAssert("Channel 2 loaded", () => chatOverlay.GetSlowLoadingChannel(testChannel2).IsLoaded);
|
||||
AddAssert("Channel 2 displayed", () => channelIsVisible && currentDrawableChannel.Channel == testChannel2);
|
||||
|
||||
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
|
||||
AddAssert("Channel 1 loaded", () => chatOverlay.GetSlowLoadingChannel(testChannel1).IsLoaded);
|
||||
AddAssert("Channel 1 displayed", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
|
||||
}
|
||||
|
||||
private bool listingIsVisible =>
|
||||
chatOverlay.ChildrenOfType<ChannelListing>().Single().State.Value == Visibility.Visible;
|
||||
|
||||
@ -432,5 +462,35 @@ namespace osu.Game.Tests.Visual.Online
|
||||
Topic = $"We talk about the number {id} here",
|
||||
Type = ChannelType.Public,
|
||||
};
|
||||
|
||||
private class TestChatOverlayV2 : ChatOverlayV2
|
||||
{
|
||||
public bool SlowLoading { get; set; }
|
||||
|
||||
public SlowLoadingDrawableChannel GetSlowLoadingChannel(Channel channel) => DrawableChannels.OfType<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -3,9 +3,11 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online;
|
||||
@ -89,6 +91,8 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
|
||||
private void queueDownloads(string[] sourceFilenames, int? limit = null)
|
||||
{
|
||||
Debug.Assert(LoadState == LoadState.NotLoaded);
|
||||
|
||||
try
|
||||
{
|
||||
// Matches osu-stable, in order to provide new users with roughly the same randomised selection of bundled beatmaps.
|
||||
|
@ -22,7 +22,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
{
|
||||
public class LegacyBeatmapEncoder
|
||||
{
|
||||
public const int LATEST_VERSION = 128;
|
||||
public const int FIRST_LAZER_VERSION = 128;
|
||||
|
||||
/// <summary>
|
||||
/// osu! is generally slower than taiko, so a factor is added to increase
|
||||
@ -55,7 +55,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
|
||||
public void Encode(TextWriter writer)
|
||||
{
|
||||
writer.WriteLine($"osu file format v{LATEST_VERSION}");
|
||||
writer.WriteLine($"osu file format v{FIRST_LAZER_VERSION}");
|
||||
|
||||
writer.WriteLine();
|
||||
handleGeneral(writer);
|
||||
|
@ -111,6 +111,18 @@ namespace osu.Game.Collections
|
||||
|
||||
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>
|
||||
/// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future.
|
||||
/// </summary>
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
@ -36,22 +37,66 @@ namespace osu.Game.Database
|
||||
[Resolved]
|
||||
private CollectionManager collections { get; set; }
|
||||
|
||||
[Resolved]
|
||||
[Resolved(canBeNull: true)]
|
||||
private OsuGame game { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IDialogOverlay dialogOverlay { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
[Resolved(canBeNull: true)]
|
||||
private DesktopGameHost desktopGameHost { get; set; }
|
||||
|
||||
private StableStorage cachedStorage;
|
||||
|
||||
public bool SupportsImportFromStable => RuntimeInfo.IsDesktop;
|
||||
|
||||
public async Task ImportFromStableAsync(StableContent content)
|
||||
public void UpdateStorage(string stablePath) => cachedStorage = new StableStorage(stablePath, desktopGameHost);
|
||||
|
||||
public virtual async Task<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>();
|
||||
|
||||
Task beatmapImportTask = Task.CompletedTask;
|
||||
@ -70,20 +115,16 @@ namespace osu.Game.Database
|
||||
await Task.WhenAll(importTasks.ToArray()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<StableStorage> getStableStorage()
|
||||
public StableStorage GetCurrentStableStorage()
|
||||
{
|
||||
if (cachedStorage != null)
|
||||
return cachedStorage;
|
||||
|
||||
var stableStorage = game.GetStorageForStableInstall();
|
||||
var stableStorage = game?.GetStorageForStableInstall();
|
||||
if (stableStorage != null)
|
||||
return cachedStorage = stableStorage;
|
||||
|
||||
var taskCompletionSource = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
Schedule(() => dialogOverlay.Push(new StableDirectoryLocationDialog(taskCompletionSource)));
|
||||
string stablePath = await taskCompletionSource.Task.ConfigureAwait(false);
|
||||
|
||||
return cachedStorage = new StableStorage(stablePath, desktopGameHost);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,8 +24,14 @@ namespace osu.Game.Database
|
||||
/// <summary>
|
||||
/// Select paths to import from stable where all paths should be absolute. Default implementation iterates all directories in <see cref="ImportFromStablePath"/>.
|
||||
/// </summary>
|
||||
protected virtual IEnumerable<string> GetStableImportPaths(Storage storage) => storage.GetDirectories(ImportFromStablePath)
|
||||
.Select(path => storage.GetFullPath(path));
|
||||
protected virtual IEnumerable<string> GetStableImportPaths(Storage storage)
|
||||
{
|
||||
if (!storage.ExistsDirectory(ImportFromStablePath))
|
||||
return Enumerable.Empty<string>();
|
||||
|
||||
return storage.GetDirectories(ImportFromStablePath)
|
||||
.Select(path => storage.GetFullPath(path));
|
||||
}
|
||||
|
||||
protected readonly IModelImporter<TModel> Importer;
|
||||
|
||||
@ -34,6 +40,8 @@ namespace osu.Game.Database
|
||||
Importer = importer;
|
||||
}
|
||||
|
||||
public Task<int> GetAvailableCount(StableStorage stableStorage) => Task.Run(() => GetStableImportPaths(PrepareStableStorage(stableStorage)).Count());
|
||||
|
||||
public Task ImportFromStableAsync(StableStorage stableStorage)
|
||||
{
|
||||
var storage = PrepareStableStorage(stableStorage);
|
||||
|
@ -15,8 +15,14 @@ namespace osu.Game.Database
|
||||
protected override string ImportFromStablePath => Path.Combine("Data", "r");
|
||||
|
||||
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)
|
||||
: base(importer)
|
||||
|
@ -6,6 +6,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterfaceV2
|
||||
@ -25,7 +26,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
set => Component.ReadOnly = value;
|
||||
}
|
||||
|
||||
public string PlaceholderText
|
||||
public LocalisableString PlaceholderText
|
||||
{
|
||||
set => Component.PlaceholderText = value;
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterfaceV2
|
||||
{
|
||||
@ -44,8 +45,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
|
||||
internal class Background : CompositeDrawable
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(OverlayColourProvider overlayColourProvider, OsuColour colours)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
@ -54,7 +55,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
|
||||
InternalChild = new Box
|
||||
{
|
||||
Colour = colours.GreySeaFoamDarker,
|
||||
Colour = overlayColourProvider?.Background5 ?? colours.GreySeaFoamDarker,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
}
|
||||
|
@ -69,6 +69,26 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
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}";
|
||||
}
|
||||
}
|
||||
}
|
@ -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}";
|
||||
}
|
||||
}
|
@ -39,8 +39,11 @@ namespace osu.Game.Overlays
|
||||
private ChatTextBar textBar = 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 float dragStartChatHeight;
|
||||
|
||||
@ -173,7 +176,7 @@ namespace osu.Game.Overlays
|
||||
if (currentChannel.Value?.Id != channel.Id)
|
||||
{
|
||||
if (!channel.Joined.Value)
|
||||
channel = channelManager.JoinChannel(channel);
|
||||
channel = channelManager.JoinChannel(channel, false);
|
||||
|
||||
channelManager.CurrentChannel.Value = channel;
|
||||
}
|
||||
@ -240,38 +243,76 @@ namespace osu.Game.Overlays
|
||||
if (newChannel == null)
|
||||
{
|
||||
// null channel denotes that we should be showing the listing.
|
||||
channelListing.State.Value = Visibility.Visible;
|
||||
currentChannelContainer.Clear(false);
|
||||
channelListing.Show();
|
||||
textBar.ShowSearch.Value = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
channelListing.State.Value = Visibility.Hidden;
|
||||
channelListing.Hide();
|
||||
textBar.ShowSearch.Value = false;
|
||||
|
||||
loading.Show();
|
||||
LoadComponentAsync(new ChatOverlayDrawableChannel(newChannel), loaded =>
|
||||
if (loadedChannels.ContainsKey(newChannel))
|
||||
{
|
||||
currentChannelContainer.Clear();
|
||||
currentChannelContainer.Add(loaded);
|
||||
loading.Hide();
|
||||
});
|
||||
currentChannelContainer.Clear(false);
|
||||
currentChannelContainer.Add(loadedChannels[newChannel]);
|
||||
}
|
||||
else
|
||||
{
|
||||
loading.Show();
|
||||
|
||||
// Ensure the drawable channel is stored before async load to prevent double loading
|
||||
ChatOverlayDrawableChannel drawableChannel = CreateDrawableChannel(newChannel);
|
||||
loadedChannels.Add(newChannel, drawableChannel);
|
||||
|
||||
LoadComponentAsync(drawableChannel, loadedDrawable =>
|
||||
{
|
||||
// Ensure the current channel hasn't changed by the time the load completes
|
||||
if (currentChannel.Value != loadedDrawable.Channel)
|
||||
return;
|
||||
|
||||
// Ensure the cached reference hasn't been removed from leaving the channel
|
||||
if (!loadedChannels.ContainsKey(loadedDrawable.Channel))
|
||||
return;
|
||||
|
||||
currentChannelContainer.Clear(false);
|
||||
currentChannelContainer.Add(loadedDrawable);
|
||||
loading.Hide();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual ChatOverlayDrawableChannel CreateDrawableChannel(Channel newChannel) => new ChatOverlayDrawableChannel(newChannel);
|
||||
|
||||
private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args)
|
||||
{
|
||||
switch (args.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
IEnumerable<Channel> joinedChannels = filterChannels(args.NewItems);
|
||||
|
||||
foreach (var channel in joinedChannels)
|
||||
channelList.AddChannel(channel);
|
||||
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
IEnumerable<Channel> leftChannels = filterChannels(args.OldItems);
|
||||
|
||||
foreach (var channel in leftChannels)
|
||||
{
|
||||
channelList.RemoveChannel(channel);
|
||||
|
||||
if (loadedChannels.ContainsKey(channel))
|
||||
{
|
||||
ChatOverlayDrawableChannel loaded = loadedChannels[channel];
|
||||
loadedChannels.Remove(channel);
|
||||
// DrawableChannel removed from cache must be manually disposed
|
||||
loaded.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,8 @@ namespace osu.Game.Overlays.FirstRunSetup
|
||||
|
||||
protected const float CONTENT_FONT_SIZE = 16;
|
||||
|
||||
protected const float CONTENT_PADDING = 30;
|
||||
|
||||
protected const float HEADER_FONT_SIZE = 24;
|
||||
|
||||
[Resolved]
|
||||
@ -41,7 +43,7 @@ namespace osu.Game.Overlays.FirstRunSetup
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding { Horizontal = 30 },
|
||||
Padding = new MarginPadding { Horizontal = CONTENT_PADDING },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
|
103
osu.Game/Overlays/FirstRunSetup/ProgressRoundedButton.cs
Normal file
103
osu.Game/Overlays/FirstRunSetup/ProgressRoundedButton.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -14,8 +14,6 @@ using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online;
|
||||
using osuTK;
|
||||
@ -27,7 +25,6 @@ namespace osu.Game.Overlays.FirstRunSetup
|
||||
public class ScreenBeatmaps : FirstRunSetupScreen
|
||||
{
|
||||
private ProgressRoundedButton downloadBundledButton = null!;
|
||||
private ProgressRoundedButton importBeatmapsButton = null!;
|
||||
private ProgressRoundedButton downloadTutorialButton = null!;
|
||||
|
||||
private OsuTextFlowContainer currentlyLoadedBeatmaps = null!;
|
||||
@ -43,8 +40,8 @@ namespace osu.Game.Overlays.FirstRunSetup
|
||||
|
||||
private IDisposable? beatmapSubscription;
|
||||
|
||||
[BackgroundDependencyLoader(permitNulls: true)]
|
||||
private void load(LegacyImportManager? legacyImportManager)
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Vector2 buttonSize = new Vector2(400, 50);
|
||||
|
||||
@ -106,32 +103,6 @@ namespace osu.Game.Overlays.FirstRunSetup
|
||||
Action = downloadBundled
|
||||
},
|
||||
new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
|
||||
{
|
||||
Colour = OverlayColourProvider.Content1,
|
||||
Text = "If you have an existing osu! install, you can also choose to import your existing beatmap collection.",
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y
|
||||
},
|
||||
importBeatmapsButton = new ProgressRoundedButton
|
||||
{
|
||||
Size = buttonSize,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
BackgroundColour = colours.Blue3,
|
||||
Text = MaintenanceSettingsStrings.ImportBeatmapsFromStable,
|
||||
Action = () =>
|
||||
{
|
||||
importBeatmapsButton.Enabled.Value = false;
|
||||
legacyImportManager?.ImportFromStableAsync(StableContent.Beatmaps).ContinueWith(t => Schedule(() =>
|
||||
{
|
||||
if (t.IsCompletedSuccessfully)
|
||||
importBeatmapsButton.Complete();
|
||||
else
|
||||
importBeatmapsButton.Enabled.Value = true;
|
||||
}));
|
||||
}
|
||||
},
|
||||
new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
|
||||
{
|
||||
Colour = OverlayColourProvider.Content1,
|
||||
Text = FirstRunSetupBeatmapScreenStrings.ObtainMoreBeatmaps,
|
||||
@ -214,45 +185,5 @@ namespace osu.Game.Overlays.FirstRunSetup
|
||||
downloadBundledButton.SetProgress(progress, true);
|
||||
}
|
||||
}
|
||||
|
||||
private class ProgressRoundedButton : RoundedButton
|
||||
{
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
private ProgressBar progressBar = null!;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Add(progressBar = new ProgressBar(false)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Blending = BlendingParameters.Additive,
|
||||
FillColour = BackgroundColour,
|
||||
Alpha = 0.5f,
|
||||
Depth = float.MinValue
|
||||
});
|
||||
}
|
||||
|
||||
public void Complete()
|
||||
{
|
||||
Enabled.Value = false;
|
||||
|
||||
Background.FadeColour(colours.Green, 500, Easing.OutQuint);
|
||||
progressBar.FillColour = colours.Green;
|
||||
|
||||
this.TransformBindableTo(progressBar.Current, 1, 500, Easing.OutQuint);
|
||||
}
|
||||
|
||||
public void SetProgress(double progress, bool animated)
|
||||
{
|
||||
if (!Enabled.Value)
|
||||
return;
|
||||
|
||||
this.TransformBindableTo(progressBar.Current, progress, animated ? 500 : 0, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
267
osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs
Normal file
267
osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs
Normal 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 }
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -4,12 +4,14 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
@ -17,6 +19,7 @@ using osu.Framework.Localisation;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input.Bindings;
|
||||
@ -55,18 +58,10 @@ namespace osu.Game.Overlays
|
||||
/// </summary>
|
||||
public FirstRunSetupScreen? CurrentScreen => (FirstRunSetupScreen?)stack?.CurrentScreen;
|
||||
|
||||
private readonly Type[] steps =
|
||||
{
|
||||
typeof(ScreenWelcome),
|
||||
typeof(ScreenBeatmaps),
|
||||
typeof(ScreenUIScale),
|
||||
typeof(ScreenBehaviour),
|
||||
};
|
||||
private readonly List<Type> steps = new List<Type>();
|
||||
|
||||
private Container screenContent = null!;
|
||||
|
||||
private Bindable<OverlayActivation>? overlayActivationMode;
|
||||
|
||||
private Container content = null!;
|
||||
|
||||
private LoadingSpinner loading = null!;
|
||||
@ -77,15 +72,22 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
[BackgroundDependencyLoader(permitNulls: true)]
|
||||
private void load(OsuColour colours, LegacyImportManager? legacyImportManager)
|
||||
{
|
||||
steps.Add(typeof(ScreenWelcome));
|
||||
steps.Add(typeof(ScreenBeatmaps));
|
||||
if (legacyImportManager?.SupportsImportFromStable == true)
|
||||
steps.Add(typeof(ScreenImportFromStable));
|
||||
steps.Add(typeof(ScreenUIScale));
|
||||
steps.Add(typeof(ScreenBehaviour));
|
||||
|
||||
Header.Title = FirstRunSetupOverlayStrings.FirstRunSetupTitle;
|
||||
Header.Description = FirstRunSetupOverlayStrings.FirstRunSetupDescription;
|
||||
|
||||
MainAreaContent.AddRange(new Drawable[]
|
||||
{
|
||||
content = new Container
|
||||
content = new PopoverContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@ -221,16 +223,9 @@ namespace osu.Game.Overlays
|
||||
// if we are valid for display, only do so after reaching the main menu.
|
||||
performer.PerformFromScreen(screen =>
|
||||
{
|
||||
MainMenu menu = (MainMenu)screen;
|
||||
|
||||
// Eventually I'd like to replace this with a better method that doesn't access the screen.
|
||||
// Either this dialog would be converted to its own screen, or at very least be "hosted" by a screen pushed to the main menu.
|
||||
// Alternatively, another method of disabling notifications could be added to `INotificationOverlay`.
|
||||
if (menu != null)
|
||||
{
|
||||
overlayActivationMode = menu.OverlayActivationMode.GetBoundCopy();
|
||||
overlayActivationMode.Value = OverlayActivation.UserTriggered;
|
||||
}
|
||||
// Hides the toolbar for us.
|
||||
if (screen is MainMenu menu)
|
||||
menu.ReturnToOsuLogo();
|
||||
|
||||
base.Show();
|
||||
}, new[] { typeof(MainMenu) });
|
||||
@ -253,13 +248,6 @@ namespace osu.Game.Overlays
|
||||
|
||||
content.ScaleTo(0.99f, 400, Easing.OutQuint);
|
||||
|
||||
if (overlayActivationMode != null)
|
||||
{
|
||||
// If this is non-null we are guaranteed to have come from the main menu.
|
||||
overlayActivationMode.Value = OverlayActivation.All;
|
||||
overlayActivationMode = null;
|
||||
}
|
||||
|
||||
if (currentStepIndex != null)
|
||||
{
|
||||
notificationOverlay.Post(new SimpleNotification
|
||||
@ -313,7 +301,7 @@ namespace osu.Game.Overlays
|
||||
|
||||
currentStepIndex++;
|
||||
|
||||
if (currentStepIndex < steps.Length)
|
||||
if (currentStepIndex < steps.Count)
|
||||
{
|
||||
var nextScreen = (Screen)Activator.CreateInstance(steps[currentStepIndex.Value]);
|
||||
|
||||
@ -345,7 +333,7 @@ namespace osu.Game.Overlays
|
||||
return;
|
||||
|
||||
bool isFirstStep = currentStepIndex == 0;
|
||||
bool isLastStep = currentStepIndex == steps.Length - 1;
|
||||
bool isLastStep = currentStepIndex == steps.Count - 1;
|
||||
|
||||
if (isFirstStep)
|
||||
{
|
||||
|
@ -428,9 +428,8 @@ namespace osu.Game.Overlays.Mods
|
||||
base.PopIn();
|
||||
|
||||
multiplierDisplay?
|
||||
.Delay(fade_in_duration * 0.65f)
|
||||
.FadeIn(fade_in_duration / 2, Easing.OutQuint)
|
||||
.ScaleTo(1, fade_in_duration, Easing.OutElastic);
|
||||
.FadeIn(fade_in_duration, Easing.OutQuint)
|
||||
.MoveToY(0, fade_in_duration, Easing.OutQuint);
|
||||
|
||||
int nonFilteredColumnCount = 0;
|
||||
|
||||
@ -465,7 +464,7 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
multiplierDisplay?
|
||||
.FadeOut(fade_out_duration / 2, Easing.OutQuint)
|
||||
.ScaleTo(0.75f, fade_out_duration, Easing.OutQuint);
|
||||
.MoveToY(-distance, fade_out_duration / 2, Easing.OutQuint);
|
||||
|
||||
int nonFilteredColumnCount = 0;
|
||||
|
||||
|
@ -7,6 +7,7 @@ using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
@ -361,20 +362,23 @@ namespace osu.Game.Rulesets.Edit
|
||||
/// <returns>The most relevant <see cref="Playfield"/>.</returns>
|
||||
protected virtual Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => drawableRulesetWrapper.Playfield;
|
||||
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||
{
|
||||
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
|
||||
double? targetTime = null;
|
||||
|
||||
if (playfield is ScrollingPlayfield scrollingPlayfield)
|
||||
if (snapType.HasFlagFast(SnapType.Grids))
|
||||
{
|
||||
targetTime = scrollingPlayfield.TimeAtScreenSpacePosition(screenSpacePosition);
|
||||
if (playfield is ScrollingPlayfield scrollingPlayfield)
|
||||
{
|
||||
targetTime = scrollingPlayfield.TimeAtScreenSpacePosition(screenSpacePosition);
|
||||
|
||||
// apply beat snapping
|
||||
targetTime = BeatSnapProvider.SnapTime(targetTime.Value);
|
||||
// apply beat snapping
|
||||
targetTime = BeatSnapProvider.SnapTime(targetTime.Value);
|
||||
|
||||
// convert back to screen space
|
||||
screenSpacePosition = scrollingPlayfield.ScreenSpacePositionAtTime(targetTime.Value);
|
||||
// convert back to screen space
|
||||
screenSpacePosition = scrollingPlayfield.ScreenSpacePositionAtTime(targetTime.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return new SnapResult(screenSpacePosition, targetTime, playfield);
|
||||
@ -414,10 +418,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
|
||||
#region IPositionSnapProvider
|
||||
|
||||
public abstract SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition);
|
||||
|
||||
public virtual SnapResult FindSnappedPosition(Vector2 screenSpacePosition) =>
|
||||
new SnapResult(screenSpacePosition, null);
|
||||
public abstract SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All);
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ namespace osu.Game.Rulesets.Edit
|
||||
{
|
||||
/// <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.
|
||||
/// Provided values are inferred in an isolated context, without consideration of other nearby hit objects.
|
||||
/// </summary>
|
||||
[Cached]
|
||||
public interface IPositionSnapProvider
|
||||
@ -16,18 +15,9 @@ namespace osu.Game.Rulesets.Edit
|
||||
/// <summary>
|
||||
/// Given a position, find a valid time and position snap.
|
||||
/// </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="snapType">The type of snapping to apply.</param>
|
||||
/// <returns>The time and position post-snapping.</returns>
|
||||
SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition);
|
||||
|
||||
/// <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);
|
||||
SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All);
|
||||
}
|
||||
}
|
||||
|
16
osu.Game/Rulesets/Edit/SnapType.cs
Normal file
16
osu.Game/Rulesets/Edit/SnapType.cs
Normal 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,
|
||||
}
|
||||
}
|
@ -336,10 +336,14 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
|
||||
while (++endIndex < vertices.Length - endPointLength)
|
||||
{
|
||||
// Keep incrementing while an implicit segment doesn't need to be started
|
||||
// Keep incrementing while an implicit segment doesn't need to be started.
|
||||
if (vertices[endIndex].Position != vertices[endIndex - 1].Position)
|
||||
continue;
|
||||
|
||||
// Adjacent legacy Catmull segments should be treated as a single segment.
|
||||
if (FormatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION && type == PathType.Catmull)
|
||||
continue;
|
||||
|
||||
// The last control point of each segment is not allowed to start a new implicit segment.
|
||||
if (endIndex == vertices.Length - endPointLength - 1)
|
||||
continue;
|
||||
|
@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
public new ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)base.HitObjectContainer;
|
||||
|
||||
[Resolved]
|
||||
protected IScrollingInfo ScrollingInfo { get; private set; }
|
||||
public IScrollingInfo ScrollingInfo { get; private set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
|
@ -486,7 +486,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
Vector2 originalPosition = movementBlueprintOriginalPositions[i];
|
||||
var testPosition = originalPosition + distanceTravelled;
|
||||
|
||||
var positionalResult = snapProvider.FindSnappedPosition(testPosition);
|
||||
var positionalResult = snapProvider.FindSnappedPositionAndTime(testPosition, SnapType.NearbyObjects);
|
||||
|
||||
if (positionalResult.ScreenSpacePosition == testPosition) continue;
|
||||
|
||||
@ -505,7 +505,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
Vector2 movePosition = movementBlueprintOriginalPositions.First() + distanceTravelled;
|
||||
|
||||
// Retrieve a snapped position.
|
||||
var result = snapProvider?.FindSnappedPositionAndTime(movePosition);
|
||||
var result = snapProvider?.FindSnappedPositionAndTime(movePosition, ~SnapType.NearbyObjects);
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
|
@ -154,7 +154,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
private void updateBankPlaceholderText(IEnumerable<HitObject> objects)
|
||||
{
|
||||
string? commonBank = getCommonBank(objects.Select(h => h.SampleControlPoint).ToArray());
|
||||
bank.PlaceholderText = string.IsNullOrEmpty(commonBank) ? "(multiple)" : null;
|
||||
bank.PlaceholderText = string.IsNullOrEmpty(commonBank) ? "(multiple)" : string.Empty;
|
||||
}
|
||||
|
||||
private void updateVolumeFor(IEnumerable<HitObject> objects, int? newVolume)
|
||||
|
@ -303,10 +303,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
/// </summary>
|
||||
public double VisibleRange => track.Length / Zoom;
|
||||
|
||||
public SnapResult FindSnappedPosition(Vector2 screenSpacePosition) =>
|
||||
new SnapResult(screenSpacePosition, null);
|
||||
|
||||
public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) =>
|
||||
public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) =>
|
||||
new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition))));
|
||||
|
||||
private double getTimeFromPosition(Vector2 localPosition) =>
|
||||
|
@ -11,11 +11,8 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osuTK;
|
||||
|
||||
@ -24,7 +21,7 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
/// <summary>
|
||||
/// A labelled textbox which reveals an inline file chooser when clicked.
|
||||
/// </summary>
|
||||
internal class FileChooserLabelledTextBox : LabelledTextBox, ICanAcceptFiles, IHasPopover
|
||||
internal class FileChooserLabelledTextBox : LabelledTextBoxWithPopover, ICanAcceptFiles
|
||||
{
|
||||
private readonly string[] handledExtensions;
|
||||
|
||||
@ -40,16 +37,6 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
this.handledExtensions = handledExtensions;
|
||||
}
|
||||
|
||||
protected override OsuTextBox CreateTextBox() =>
|
||||
new FileChooserOsuTextBox
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
CornerRadius = CORNER_RADIUS,
|
||||
OnFocused = this.ShowPopover
|
||||
};
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
@ -81,27 +68,7 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
game.UnregisterImportHandler(this);
|
||||
}
|
||||
|
||||
internal class FileChooserOsuTextBox : OsuTextBox
|
||||
{
|
||||
public Action OnFocused;
|
||||
|
||||
protected override bool OnDragStart(DragStartEvent e)
|
||||
{
|
||||
// This text box is intended to be "read only" without actually specifying that.
|
||||
// As such we don't want to allow the user to select its content with a drag.
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void OnFocus(FocusEvent e)
|
||||
{
|
||||
OnFocused?.Invoke();
|
||||
base.OnFocus(e);
|
||||
|
||||
GetContainingInputManager().TriggerFocusContention(this);
|
||||
}
|
||||
}
|
||||
|
||||
public Popover GetPopover() => new FileChooserPopover(handledExtensions, currentFile);
|
||||
public override Popover GetPopover() => new FileChooserPopover(handledExtensions, currentFile);
|
||||
|
||||
private class FileChooserPopover : OsuPopover
|
||||
{
|
||||
|
52
osu.Game/Screens/Edit/Setup/LabelledTextBoxWithPopover.cs
Normal file
52
osu.Game/Screens/Edit/Setup/LabelledTextBoxWithPopover.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -150,6 +150,8 @@ namespace osu.Game.Screens.Menu
|
||||
[Resolved(canBeNull: true)]
|
||||
private IPerformFromScreenRunner performer { get; set; }
|
||||
|
||||
public void ReturnToOsuLogo() => Buttons.State = ButtonSystemState.Initial;
|
||||
|
||||
private void confirmAndExit()
|
||||
{
|
||||
if (exitConfirmed) return;
|
||||
|
@ -8,6 +8,7 @@ using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Logging;
|
||||
@ -160,6 +161,8 @@ namespace osu.Game.Utils
|
||||
};
|
||||
|
||||
scope.SetTag(@"ruleset", ruleset.ShortName);
|
||||
scope.SetTag(@"os", $"{RuntimeInfo.OS} ({Environment.OSVersion})");
|
||||
scope.SetTag(@"processor count", Environment.ProcessorCount.ToString());
|
||||
});
|
||||
}
|
||||
else
|
||||
|
@ -34,7 +34,7 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="10.12.0" />
|
||||
<PackageReference Include="Realm" Version="10.11.2" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2022.520.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.513.0" />
|
||||
<PackageReference Include="Sentry" Version="3.17.1" />
|
||||
|
@ -89,6 +89,6 @@
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.NativeLibs" Version="2022.429.0" ExcludeAssets="all" />
|
||||
<PackageReference Include="Realm" Version="10.12.0" />
|
||||
<PackageReference Include="Realm" Version="10.11.2" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -33,6 +33,8 @@
|
||||
<true/>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
|
Loading…
Reference in New Issue
Block a user