mirror of
https://github.com/ppy/osu
synced 2025-04-01 22:48:33 +00:00
Merge branch 'master' into new-chat-remove-selector-item
This commit is contained in:
commit
fb06b7658b
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -31,7 +31,7 @@ jobs:
|
|||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: ${{ github.workspace }}/inspectcode
|
path: ${{ github.workspace }}/inspectcode
|
||||||
key: inspectcode-${{ hashFiles('.config/dotnet-tools.json', '.github/workflows/ci.yml', 'osu.sln*', '.editorconfig', '.globalconfig') }}
|
key: inspectcode-${{ hashFiles('.config/dotnet-tools.json', '.github/workflows/ci.yml', 'osu.sln*', '.editorconfig', '.globalconfig', 'CodeAnalysis/*') }}
|
||||||
|
|
||||||
- name: Dotnet code style
|
- name: Dotnet code style
|
||||||
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf -p:EnforceCodeStyleInBuild=true
|
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf -p:EnforceCodeStyleInBuild=true
|
||||||
|
26
.github/workflows/sentry-release.yml
vendored
Normal file
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
|
# Fody (pulled in by Realm) - schema file
|
||||||
FodyWeavers.xsd
|
FodyWeavers.xsd
|
||||||
**/FodyWeavers.xml
|
**/FodyWeavers.xml
|
||||||
|
|
||||||
|
.idea/.idea.osu.Desktop/.idea/misc.xml
|
11
.idea/.idea.osu.Desktop/.idea/misc.xml
generated
11
.idea/.idea.osu.Desktop/.idea/misc.xml
generated
@ -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.
|
T:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods.
|
||||||
M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast<T>() instead.
|
M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast<T>() instead.
|
||||||
M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection<T>,NotificationCallbackDelegate<T>) instead.
|
M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection<T>,NotificationCallbackDelegate<T>) instead.
|
||||||
|
M:System.Guid.#ctor;Probably meaning to use Guid.NewGuid() instead. If actually wanting empty, use Guid.Empty.
|
||||||
M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IQueryable<T>,NotificationCallbackDelegate<T>) instead.
|
M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IQueryable<T>,NotificationCallbackDelegate<T>) instead.
|
||||||
M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IList<T>,NotificationCallbackDelegate<T>) instead.
|
M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IList<T>,NotificationCallbackDelegate<T>) instead.
|
||||||
M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks.
|
M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks.
|
||||||
|
@ -52,10 +52,10 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.513.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.513.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.511.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.521.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Transitive Dependencies">
|
<ItemGroup Label="Transitive Dependencies">
|
||||||
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
||||||
<PackageReference Include="Realm" Version="10.12.0" />
|
<PackageReference Include="Realm" Version="10.11.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -18,5 +18,6 @@
|
|||||||
<file src="**.exe" target="lib\net45\" exclude="**vshost**"/>
|
<file src="**.exe" target="lib\net45\" exclude="**vshost**"/>
|
||||||
<file src="**.dll" target="lib\net45\"/>
|
<file src="**.dll" target="lib\net45\"/>
|
||||||
<file src="**.config" target="lib\net45\"/>
|
<file src="**.config" target="lib\net45\"/>
|
||||||
|
<file src="**.json" target="lib\net45\"/>
|
||||||
</files>
|
</files>
|
||||||
</package>
|
</package>
|
||||||
|
@ -37,6 +37,8 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>XSAppIconAssets</key>
|
<key>XSAppIconAssets</key>
|
||||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||||
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
<true/>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
{
|
{
|
||||||
public class TestSceneJuiceStreamPlacementBlueprint : CatchPlacementBlueprintTestScene
|
public class TestSceneJuiceStreamPlacementBlueprint : CatchPlacementBlueprintTestScene
|
||||||
{
|
{
|
||||||
private const double velocity = 0.5;
|
private const double velocity_factor = 0.5;
|
||||||
|
|
||||||
private JuiceStream lastObject => LastObject?.HitObject as JuiceStream;
|
private JuiceStream lastObject => LastObject?.HitObject as JuiceStream;
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
{
|
{
|
||||||
var playable = base.GetPlayableBeatmap();
|
var playable = base.GetPlayableBeatmap();
|
||||||
playable.Difficulty.SliderTickRate = 5;
|
playable.Difficulty.SliderTickRate = 5;
|
||||||
playable.Difficulty.SliderMultiplier = velocity * 10;
|
playable.Difficulty.SliderMultiplier = velocity_factor * 10;
|
||||||
return playable;
|
return playable;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,6 +43,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
AddAssert("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1]));
|
AddAssert("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1]));
|
||||||
AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0]));
|
AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0]));
|
||||||
AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1]));
|
AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1]));
|
||||||
|
AddAssert("default slider velocity", () => lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -66,28 +67,21 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestVelocityLimit()
|
public void TestSliderVelocityChange()
|
||||||
{
|
{
|
||||||
double[] times = { 100, 300 };
|
double[] times = { 100, 300 };
|
||||||
float[] positions = { 200, 500 };
|
float[] positions = { 200, 500 };
|
||||||
addPlacementSteps(times, positions);
|
addPlacementSteps(times, positions);
|
||||||
addPathCheckStep(times, new float[] { 200, 300 });
|
addPathCheckStep(times, positions);
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
AddAssert("slider velocity changed", () => !lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
|
||||||
public void TestPreviousVerticesAreFixed()
|
|
||||||
{
|
|
||||||
double[] times = { 100, 300, 500, 700 };
|
|
||||||
float[] positions = { 200, 400, 100, 500 };
|
|
||||||
addPlacementSteps(times, positions);
|
|
||||||
addPathCheckStep(times, new float[] { 200, 300, 200, 300 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestClampedPositionIsRestored()
|
public void TestClampedPositionIsRestored()
|
||||||
{
|
{
|
||||||
double[] times = { 100, 300, 500 };
|
double[] times = { 100, 300, 500 };
|
||||||
float[] positions = { 200, 200, 0, 250 };
|
float[] positions = { 200, 200, -3000, 250 };
|
||||||
|
|
||||||
addMoveAndClickSteps(times[0], positions[0]);
|
addMoveAndClickSteps(times[0], positions[0]);
|
||||||
addMoveAndClickSteps(times[1], positions[1]);
|
addMoveAndClickSteps(times[1], positions[1]);
|
||||||
@ -97,15 +91,6 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
addPathCheckStep(times, new float[] { 200, 200, 250 });
|
addPathCheckStep(times, new float[] { 200, 200, 250 });
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestFirstVertexIsFixed()
|
|
||||||
{
|
|
||||||
double[] times = { 100, 200 };
|
|
||||||
float[] positions = { 100, 300 };
|
|
||||||
addPlacementSteps(times, positions);
|
|
||||||
addPathCheckStep(times, new float[] { 100, 150 });
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestOutOfOrder()
|
public void TestOutOfOrder()
|
||||||
{
|
{
|
||||||
|
@ -101,31 +101,16 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestClampedPositionIsRestored()
|
public void TestSliderVelocityChange()
|
||||||
{
|
{
|
||||||
const double velocity = 0.25;
|
double[] times = { 100, 300 };
|
||||||
double[] times = { 100, 500, 700 };
|
float[] positions = { 200, 300 };
|
||||||
float[] positions = { 100, 100, 100 };
|
addBlueprintStep(times, positions);
|
||||||
addBlueprintStep(times, positions, velocity);
|
AddAssert("default slider velocity", () => hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
|
||||||
|
|
||||||
addDragStartStep(times[1], positions[1]);
|
addDragStartStep(times[1], positions[1]);
|
||||||
|
AddMouseMoveStep(times[1], 400);
|
||||||
AddMouseMoveStep(times[1], 200);
|
AddAssert("slider velocity changed", () => !hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
|
||||||
addVertexCheckStep(3, 1, times[1], 200);
|
|
||||||
addVertexCheckStep(3, 2, times[2], 150);
|
|
||||||
|
|
||||||
AddMouseMoveStep(times[1], 100);
|
|
||||||
addVertexCheckStep(3, 1, times[1], 100);
|
|
||||||
// Stored position is restored.
|
|
||||||
addVertexCheckStep(3, 2, times[2], positions[2]);
|
|
||||||
|
|
||||||
AddMouseMoveStep(times[1], 300);
|
|
||||||
addDragEndStep();
|
|
||||||
addDragStartStep(times[1], 300);
|
|
||||||
|
|
||||||
AddMouseMoveStep(times[1], 100);
|
|
||||||
// Position is different because a changed position is committed when the previous drag is ended.
|
|
||||||
addVertexCheckStep(3, 2, times[2], 250);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -174,7 +159,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
addAddVertexSteps(500, 150);
|
addAddVertexSteps(500, 150);
|
||||||
addVertexCheckStep(3, 1, 500, 150);
|
addVertexCheckStep(3, 1, 500, 150);
|
||||||
|
|
||||||
addAddVertexSteps(90, 220);
|
addAddVertexSteps(90, 200);
|
||||||
addVertexCheckStep(4, 1, times[0], positions[0]);
|
addVertexCheckStep(4, 1, times[0], positions[0]);
|
||||||
|
|
||||||
addAddVertexSteps(750, 180);
|
addAddVertexSteps(750, 180);
|
||||||
@ -234,10 +219,10 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
{
|
{
|
||||||
var path = new JuiceStreamPath();
|
var path = new JuiceStreamPath();
|
||||||
for (int i = 1; i < times.Length; i++)
|
for (int i = 1; i < times.Length; i++)
|
||||||
path.Add((times[i] - times[0]) * velocity, positions[i] - positions[0]);
|
path.Add(times[i] - times[0], positions[i] - positions[0]);
|
||||||
|
|
||||||
var sliderPath = new SliderPath();
|
var sliderPath = new SliderPath();
|
||||||
path.ConvertToSliderPath(sliderPath, 0);
|
path.ConvertToSliderPath(sliderPath, 0, velocity);
|
||||||
addBlueprintStep(times[0], positions[0], sliderPath, velocity);
|
addBlueprintStep(times[0], positions[0], sliderPath, velocity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -245,11 +230,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
|
|
||||||
private void addVertexCheckStep(int count, int index, double time, float x) => AddAssert($"vertex {index} of {count} at {time}, {x}", () =>
|
private void addVertexCheckStep(int count, int index, double time, float x) => AddAssert($"vertex {index} of {count} at {time}, {x}", () =>
|
||||||
{
|
{
|
||||||
double expectedDistance = (time - hitObject.StartTime) * hitObject.Velocity;
|
double expectedTime = time - hitObject.StartTime;
|
||||||
float expectedX = x - hitObject.OriginalX;
|
float expectedX = x - hitObject.OriginalX;
|
||||||
var vertices = getVertices();
|
var vertices = getVertices();
|
||||||
return vertices.Count == count &&
|
return vertices.Count == count &&
|
||||||
Precision.AlmostEquals(vertices[index].Distance, expectedDistance, 1e-3) &&
|
Precision.AlmostEquals(vertices[index].Time, expectedTime, 1e-3) &&
|
||||||
Precision.AlmostEquals(vertices[index].X, expectedX);
|
Precision.AlmostEquals(vertices[index].X, expectedX);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Utils;
|
|
||||||
using osu.Game.Rulesets.Catch.Objects;
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
@ -37,14 +36,14 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
{
|
{
|
||||||
case 0:
|
case 0:
|
||||||
{
|
{
|
||||||
double distance = rng.NextDouble() * scale * 2 - scale;
|
double time = rng.NextDouble() * scale * 2 - scale;
|
||||||
if (integralValues)
|
if (integralValues)
|
||||||
distance = Math.Round(distance);
|
time = Math.Round(time);
|
||||||
|
|
||||||
float oldX = path.PositionAtDistance(distance);
|
float oldX = path.PositionAtTime(time);
|
||||||
int index = path.InsertVertex(distance);
|
int index = path.InsertVertex(time);
|
||||||
Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount + 1));
|
Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount + 1));
|
||||||
Assert.That(path.Vertices[index].Distance, Is.EqualTo(distance));
|
Assert.That(path.Vertices[index].Time, Is.EqualTo(time));
|
||||||
Assert.That(path.Vertices[index].X, Is.EqualTo(oldX));
|
Assert.That(path.Vertices[index].X, Is.EqualTo(oldX));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -52,20 +51,20 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
case 1:
|
case 1:
|
||||||
{
|
{
|
||||||
int index = rng.Next(path.Vertices.Count);
|
int index = rng.Next(path.Vertices.Count);
|
||||||
double distance = path.Vertices[index].Distance;
|
double time = path.Vertices[index].Time;
|
||||||
float newX = (float)(rng.NextDouble() * scale * 2 - scale);
|
float newX = (float)(rng.NextDouble() * scale * 2 - scale);
|
||||||
if (integralValues)
|
if (integralValues)
|
||||||
newX = MathF.Round(newX);
|
newX = MathF.Round(newX);
|
||||||
|
|
||||||
path.SetVertexPosition(index, newX);
|
path.SetVertexPosition(index, newX);
|
||||||
Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount));
|
Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount));
|
||||||
Assert.That(path.Vertices[index].Distance, Is.EqualTo(distance));
|
Assert.That(path.Vertices[index].Time, Is.EqualTo(time));
|
||||||
Assert.That(path.Vertices[index].X, Is.EqualTo(newX));
|
Assert.That(path.Vertices[index].X, Is.EqualTo(newX));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assertInvariants(path.Vertices, checkSlope);
|
assertInvariants(path.Vertices);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
path.Add(10, 5);
|
path.Add(10, 5);
|
||||||
path.Add(20, -5);
|
path.Add(20, -5);
|
||||||
|
|
||||||
int removeCount = path.RemoveVertices((v, i) => v.Distance == 10 && i == 1);
|
int removeCount = path.RemoveVertices((v, i) => v.Time == 10 && i == 1);
|
||||||
Assert.That(removeCount, Is.EqualTo(1));
|
Assert.That(removeCount, Is.EqualTo(1));
|
||||||
Assert.That(path.Vertices, Is.EqualTo(new[]
|
Assert.That(path.Vertices, Is.EqualTo(new[]
|
||||||
{
|
{
|
||||||
@ -131,8 +130,9 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[TestCase(10)]
|
||||||
public void TestRandomConvertFromSliderPath()
|
[TestCase(0.1)]
|
||||||
|
public void TestRandomConvertFromSliderPath(double velocity)
|
||||||
{
|
{
|
||||||
var rng = new Random(1);
|
var rng = new Random(1);
|
||||||
var path = new JuiceStreamPath();
|
var path = new JuiceStreamPath();
|
||||||
@ -162,28 +162,28 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
else
|
else
|
||||||
sliderPath.ExpectedDistance.Value = null;
|
sliderPath.ExpectedDistance.Value = null;
|
||||||
|
|
||||||
path.ConvertFromSliderPath(sliderPath);
|
path.ConvertFromSliderPath(sliderPath, velocity);
|
||||||
Assert.That(path.Vertices[0].Distance, Is.EqualTo(0));
|
Assert.That(path.Vertices[0].Time, Is.EqualTo(0));
|
||||||
Assert.That(path.Distance, Is.EqualTo(sliderPath.Distance).Within(1e-3));
|
Assert.That(path.Duration * velocity, Is.EqualTo(sliderPath.Distance).Within(1e-3));
|
||||||
assertInvariants(path.Vertices, true);
|
assertInvariants(path.Vertices);
|
||||||
|
|
||||||
double[] sampleDistances = Enumerable.Range(0, 10)
|
double[] sampleTimes = Enumerable.Range(0, 10)
|
||||||
.Select(_ => rng.NextDouble() * sliderPath.Distance)
|
.Select(_ => rng.NextDouble() * sliderPath.Distance / velocity)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
foreach (double distance in sampleDistances)
|
foreach (double time in sampleTimes)
|
||||||
{
|
{
|
||||||
float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X;
|
float expected = sliderPath.PositionAt(time * velocity / sliderPath.Distance).X;
|
||||||
Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3));
|
Assert.That(path.PositionAtTime(time), Is.EqualTo(expected).Within(1e-3));
|
||||||
}
|
}
|
||||||
|
|
||||||
path.ResampleVertices(sampleDistances);
|
path.ResampleVertices(sampleTimes);
|
||||||
assertInvariants(path.Vertices, true);
|
assertInvariants(path.Vertices);
|
||||||
|
|
||||||
foreach (double distance in sampleDistances)
|
foreach (double time in sampleTimes)
|
||||||
{
|
{
|
||||||
float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X;
|
float expected = sliderPath.PositionAt(time * velocity / sliderPath.Distance).X;
|
||||||
Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3));
|
Assert.That(path.PositionAtTime(time), Is.EqualTo(expected).Within(1e-3));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -201,17 +201,17 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
double distance = rng.NextDouble() * 1e3;
|
double time = rng.NextDouble() * 1e3;
|
||||||
float x = (float)(rng.NextDouble() * 1e3);
|
float x = (float)(rng.NextDouble() * 1e3);
|
||||||
path.Add(distance, x);
|
path.Add(time, x);
|
||||||
} while (rng.Next(5) != 0);
|
} while (rng.Next(5) != 0);
|
||||||
|
|
||||||
float sliderStartY = (float)(rng.NextDouble() * JuiceStreamPath.OSU_PLAYFIELD_HEIGHT);
|
float sliderStartY = (float)(rng.NextDouble() * JuiceStreamPath.OSU_PLAYFIELD_HEIGHT);
|
||||||
|
|
||||||
path.ConvertToSliderPath(sliderPath, sliderStartY);
|
double requiredVelocity = path.ComputeRequiredVelocity();
|
||||||
Assert.That(sliderPath.Distance, Is.EqualTo(path.Distance).Within(1e-3));
|
double velocity = Math.Clamp(requiredVelocity, 1, 100);
|
||||||
Assert.That(sliderPath.ControlPoints[0].Position.X, Is.EqualTo(path.Vertices[0].X));
|
|
||||||
assertInvariants(path.Vertices, true);
|
path.ConvertToSliderPath(sliderPath, sliderStartY, velocity);
|
||||||
|
|
||||||
foreach (var point in sliderPath.ControlPoints)
|
foreach (var point in sliderPath.ControlPoints)
|
||||||
{
|
{
|
||||||
@ -219,11 +219,18 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
Assert.That(sliderStartY + point.Position.Y, Is.InRange(0, JuiceStreamPath.OSU_PLAYFIELD_HEIGHT));
|
Assert.That(sliderStartY + point.Position.Y, Is.InRange(0, JuiceStreamPath.OSU_PLAYFIELD_HEIGHT));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Assert.That(sliderPath.ControlPoints[0].Position.X, Is.EqualTo(path.Vertices[0].X));
|
||||||
|
|
||||||
|
// The path is preserved only if required velocity is used.
|
||||||
|
if (velocity < requiredVelocity) continue;
|
||||||
|
|
||||||
|
Assert.That(sliderPath.Distance / velocity, Is.EqualTo(path.Duration).Within(1e-3));
|
||||||
|
|
||||||
for (int i = 0; i < 10; i++)
|
for (int i = 0; i < 10; i++)
|
||||||
{
|
{
|
||||||
double distance = rng.NextDouble() * path.Distance;
|
double time = rng.NextDouble() * path.Duration;
|
||||||
float expected = path.PositionAtDistance(distance);
|
float expected = path.PositionAtTime(time);
|
||||||
Assert.That(sliderPath.PositionAt(distance / sliderPath.Distance).X, Is.EqualTo(expected).Within(1e-3));
|
Assert.That(sliderPath.PositionAt(time * velocity / sliderPath.Distance).X, Is.EqualTo(expected).Within(3e-3));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -244,7 +251,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
path.Add(20, 0);
|
path.Add(20, 0);
|
||||||
checkNewId();
|
checkNewId();
|
||||||
|
|
||||||
path.RemoveVertices((v, _) => v.Distance == 20);
|
path.RemoveVertices((v, _) => v.Time == 20);
|
||||||
checkNewId();
|
checkNewId();
|
||||||
|
|
||||||
path.ResampleVertices(new double[] { 5, 10, 15 });
|
path.ResampleVertices(new double[] { 5, 10, 15 });
|
||||||
@ -253,7 +260,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
path.Clear();
|
path.Clear();
|
||||||
checkNewId();
|
checkNewId();
|
||||||
|
|
||||||
path.ConvertFromSliderPath(new SliderPath());
|
path.ConvertFromSliderPath(new SliderPath(), 1);
|
||||||
checkNewId();
|
checkNewId();
|
||||||
|
|
||||||
void checkNewId()
|
void checkNewId()
|
||||||
@ -263,25 +270,19 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertInvariants(IReadOnlyList<JuiceStreamPathVertex> vertices, bool checkSlope)
|
private void assertInvariants(IReadOnlyList<JuiceStreamPathVertex> vertices)
|
||||||
{
|
{
|
||||||
Assert.That(vertices, Is.Not.Empty);
|
Assert.That(vertices, Is.Not.Empty);
|
||||||
|
|
||||||
for (int i = 0; i < vertices.Count; i++)
|
for (int i = 0; i < vertices.Count; i++)
|
||||||
{
|
{
|
||||||
Assert.That(double.IsFinite(vertices[i].Distance));
|
Assert.That(double.IsFinite(vertices[i].Time));
|
||||||
Assert.That(float.IsFinite(vertices[i].X));
|
Assert.That(float.IsFinite(vertices[i].X));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 1; i < vertices.Count; i++)
|
for (int i = 1; i < vertices.Count; i++)
|
||||||
{
|
{
|
||||||
Assert.That(vertices[i].Distance, Is.GreaterThanOrEqualTo(vertices[i - 1].Distance));
|
Assert.That(vertices[i].Time, Is.GreaterThanOrEqualTo(vertices[i - 1].Time));
|
||||||
|
|
||||||
if (!checkSlope) continue;
|
|
||||||
|
|
||||||
float xDiff = Math.Abs(vertices[i].X - vertices[i - 1].X);
|
|
||||||
double distanceDiff = vertices[i].Distance - vertices[i - 1].Distance;
|
|
||||||
Assert.That(xDiff, Is.LessThanOrEqualTo(distanceDiff).Within(Precision.FLOAT_EPSILON));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
|
|
||||||
public int VertexCount => path.Vertices.Count;
|
public int VertexCount => path.Vertices.Count;
|
||||||
|
|
||||||
protected readonly Func<float, double> PositionToDistance;
|
protected readonly Func<float, double> PositionToTime;
|
||||||
|
|
||||||
protected IReadOnlyList<VertexState> VertexStates => vertexStates;
|
protected IReadOnlyList<VertexState> VertexStates => vertexStates;
|
||||||
|
|
||||||
@ -44,9 +44,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
[CanBeNull]
|
[CanBeNull]
|
||||||
private IBeatSnapProvider beatSnapProvider { get; set; }
|
private IBeatSnapProvider beatSnapProvider { get; set; }
|
||||||
|
|
||||||
protected EditablePath(Func<float, double> positionToDistance)
|
protected EditablePath(Func<float, double> positionToTime)
|
||||||
{
|
{
|
||||||
PositionToDistance = positionToDistance;
|
PositionToTime = positionToTime;
|
||||||
|
|
||||||
Anchor = Anchor.BottomLeft;
|
Anchor = Anchor.BottomLeft;
|
||||||
}
|
}
|
||||||
@ -59,13 +59,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
while (InternalChildren.Count < path.Vertices.Count)
|
while (InternalChildren.Count < path.Vertices.Count)
|
||||||
AddInternal(new VertexPiece());
|
AddInternal(new VertexPiece());
|
||||||
|
|
||||||
double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity);
|
double timeToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1);
|
||||||
|
|
||||||
for (int i = 0; i < VertexCount; i++)
|
for (int i = 0; i < VertexCount; i++)
|
||||||
{
|
{
|
||||||
var piece = (VertexPiece)InternalChildren[i];
|
var piece = (VertexPiece)InternalChildren[i];
|
||||||
var vertex = path.Vertices[i];
|
var vertex = path.Vertices[i];
|
||||||
piece.Position = new Vector2(vertex.X, (float)(vertex.Distance * distanceToYFactor));
|
piece.Position = new Vector2(vertex.X, (float)(vertex.Time * timeToYFactor));
|
||||||
piece.UpdateFrom(vertexStates[i]);
|
piece.UpdateFrom(vertexStates[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -73,14 +73,14 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
public void InitializeFromHitObject(JuiceStream hitObject)
|
public void InitializeFromHitObject(JuiceStream hitObject)
|
||||||
{
|
{
|
||||||
var sliderPath = hitObject.Path;
|
var sliderPath = hitObject.Path;
|
||||||
path.ConvertFromSliderPath(sliderPath);
|
path.ConvertFromSliderPath(sliderPath, hitObject.Velocity);
|
||||||
|
|
||||||
// If the original slider path has non-linear type segments, resample the vertices at nested hit object times to reduce the number of vertices.
|
// If the original slider path has non-linear type segments, resample the vertices at nested hit object times to reduce the number of vertices.
|
||||||
if (sliderPath.ControlPoints.Any(p => p.Type != null && p.Type != PathType.Linear))
|
if (sliderPath.ControlPoints.Any(p => p.Type != null && p.Type != PathType.Linear))
|
||||||
{
|
{
|
||||||
path.ResampleVertices(hitObject.NestedHitObjects
|
path.ResampleVertices(hitObject.NestedHitObjects
|
||||||
.Skip(1).TakeWhile(h => !(h is Fruit)) // Only droplets in the first span are used.
|
.Skip(1).TakeWhile(h => !(h is Fruit)) // Only droplets in the first span are used.
|
||||||
.Select(h => (h.StartTime - hitObject.StartTime) * hitObject.Velocity));
|
.Select(h => h.StartTime - hitObject.StartTime));
|
||||||
}
|
}
|
||||||
|
|
||||||
vertexStates.Clear();
|
vertexStates.Clear();
|
||||||
@ -92,11 +92,26 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
|
|
||||||
public void UpdateHitObjectFromPath(JuiceStream hitObject)
|
public void UpdateHitObjectFromPath(JuiceStream hitObject)
|
||||||
{
|
{
|
||||||
path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY);
|
// The SV setting may need to be changed for the current path.
|
||||||
|
var svBindable = hitObject.DifficultyControlPoint.SliderVelocityBindable;
|
||||||
|
double svToVelocityFactor = hitObject.Velocity / svBindable.Value;
|
||||||
|
double requiredVelocity = path.ComputeRequiredVelocity();
|
||||||
|
|
||||||
|
// The value is pre-rounded here because setting it to the bindable will rounded to the nearest value
|
||||||
|
// but it should be always rounded up to satisfy the required minimum velocity condition.
|
||||||
|
//
|
||||||
|
// This is rounded to integers instead of using the precision of the bindable
|
||||||
|
// because it results in a smaller number of non-redundant control points.
|
||||||
|
//
|
||||||
|
// The value is clamped here by the bindable min and max values.
|
||||||
|
// In case the required velocity is too large, the path is not preserved.
|
||||||
|
svBindable.Value = Math.Ceiling(requiredVelocity / svToVelocityFactor);
|
||||||
|
|
||||||
|
path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY, hitObject.Velocity);
|
||||||
|
|
||||||
if (beatSnapProvider == null) return;
|
if (beatSnapProvider == null) return;
|
||||||
|
|
||||||
double endTime = hitObject.StartTime + path.Distance / hitObject.Velocity;
|
double endTime = hitObject.StartTime + path.Duration;
|
||||||
double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime);
|
double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime);
|
||||||
hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity;
|
hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity;
|
||||||
}
|
}
|
||||||
@ -108,9 +123,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
|
|
||||||
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
|
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
|
||||||
|
|
||||||
protected int AddVertex(double distance, float x)
|
protected int AddVertex(double time, float x)
|
||||||
{
|
{
|
||||||
int index = path.InsertVertex(distance);
|
int index = path.InsertVertex(time);
|
||||||
path.SetVertexPosition(index, x);
|
path.SetVertexPosition(index, x);
|
||||||
vertexStates.Insert(index, new VertexState());
|
vertexStates.Insert(index, new VertexState());
|
||||||
|
|
||||||
@ -138,9 +153,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void MoveSelectedVertices(double distanceDelta, float xDelta)
|
protected void MoveSelectedVertices(double timeDelta, float xDelta)
|
||||||
{
|
{
|
||||||
// Because the vertex list may be reordered due to distance change, the state list must be reordered as well.
|
// Because the vertex list may be reordered due to time change, the state list must be reordered as well.
|
||||||
previousVertexStates.Clear();
|
previousVertexStates.Clear();
|
||||||
previousVertexStates.AddRange(vertexStates);
|
previousVertexStates.AddRange(vertexStates);
|
||||||
|
|
||||||
@ -152,11 +167,11 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
for (int i = 1; i < vertexCount; i++)
|
for (int i = 1; i < vertexCount; i++)
|
||||||
{
|
{
|
||||||
var state = previousVertexStates[i];
|
var state = previousVertexStates[i];
|
||||||
double distance = state.VertexBeforeChange.Distance;
|
double time = state.VertexBeforeChange.Time;
|
||||||
if (state.IsSelected)
|
if (state.IsSelected)
|
||||||
distance += distanceDelta;
|
time += timeDelta;
|
||||||
|
|
||||||
int newIndex = path.InsertVertex(Math.Max(0, distance));
|
int newIndex = path.InsertVertex(Math.Max(0, time));
|
||||||
vertexStates.Insert(newIndex, state);
|
vertexStates.Insert(newIndex, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,15 +15,15 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private JuiceStreamPathVertex lastVertex;
|
private JuiceStreamPathVertex lastVertex;
|
||||||
|
|
||||||
public PlacementEditablePath(Func<float, double> positionToDistance)
|
public PlacementEditablePath(Func<float, double> positionToTime)
|
||||||
: base(positionToDistance)
|
: base(positionToTime)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddNewVertex()
|
public void AddNewVertex()
|
||||||
{
|
{
|
||||||
var endVertex = Vertices[^1];
|
var endVertex = Vertices[^1];
|
||||||
int index = AddVertex(endVertex.Distance, endVertex.X);
|
int index = AddVertex(endVertex.Time, endVertex.X);
|
||||||
|
|
||||||
for (int i = 0; i < VertexCount; i++)
|
for (int i = 0; i < VertexCount; i++)
|
||||||
{
|
{
|
||||||
@ -41,9 +41,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
public void MoveLastVertex(Vector2 screenSpacePosition)
|
public void MoveLastVertex(Vector2 screenSpacePosition)
|
||||||
{
|
{
|
||||||
Vector2 position = ToRelativePosition(screenSpacePosition);
|
Vector2 position = ToRelativePosition(screenSpacePosition);
|
||||||
double distanceDelta = PositionToDistance(position.Y) - lastVertex.Distance;
|
double timeDelta = PositionToTime(position.Y) - lastVertex.Time;
|
||||||
float xDelta = position.X - lastVertex.X;
|
float xDelta = position.X - lastVertex.X;
|
||||||
MoveSelectedVertices(distanceDelta, xDelta);
|
MoveSelectedVertices(timeDelta, xDelta);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
{
|
{
|
||||||
private readonly Path drawablePath;
|
private readonly Path drawablePath;
|
||||||
|
|
||||||
private readonly List<(double Distance, float X)> vertices = new List<(double, float)>();
|
private readonly List<(double Time, float X)> vertices = new List<(double, float)>();
|
||||||
|
|
||||||
public ScrollingPath()
|
public ScrollingPath()
|
||||||
{
|
{
|
||||||
@ -35,16 +35,16 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
|
|
||||||
public void UpdatePathFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject)
|
public void UpdatePathFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject)
|
||||||
{
|
{
|
||||||
double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity);
|
double timeToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1);
|
||||||
|
|
||||||
computeDistanceXs(hitObject);
|
computeTimeXs(hitObject);
|
||||||
drawablePath.Vertices = vertices
|
drawablePath.Vertices = vertices
|
||||||
.Select(v => new Vector2(v.X, (float)(v.Distance * distanceToYFactor)))
|
.Select(v => new Vector2(v.X, (float)(v.Time * timeToYFactor)))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
drawablePath.OriginPosition = drawablePath.PositionInBoundingBox(Vector2.Zero);
|
drawablePath.OriginPosition = drawablePath.PositionInBoundingBox(Vector2.Zero);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void computeDistanceXs(JuiceStream hitObject)
|
private void computeTimeXs(JuiceStream hitObject)
|
||||||
{
|
{
|
||||||
vertices.Clear();
|
vertices.Clear();
|
||||||
|
|
||||||
@ -54,17 +54,17 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
if (sliderVertices.Count == 0)
|
if (sliderVertices.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
double distance = 0;
|
double time = 0;
|
||||||
Vector2 lastPosition = Vector2.Zero;
|
Vector2 lastPosition = Vector2.Zero;
|
||||||
|
|
||||||
for (int repeat = 0; repeat < hitObject.RepeatCount + 1; repeat++)
|
for (int repeat = 0; repeat < hitObject.RepeatCount + 1; repeat++)
|
||||||
{
|
{
|
||||||
foreach (var position in sliderVertices)
|
foreach (var position in sliderVertices)
|
||||||
{
|
{
|
||||||
distance += Vector2.Distance(lastPosition, position);
|
time += Vector2.Distance(lastPosition, position) / hitObject.Velocity;
|
||||||
lastPosition = position;
|
lastPosition = position;
|
||||||
|
|
||||||
vertices.Add((distance, position.X));
|
vertices.Add((time, position.X));
|
||||||
}
|
}
|
||||||
|
|
||||||
sliderVertices.Reverse();
|
sliderVertices.Reverse();
|
||||||
|
@ -27,15 +27,15 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
[CanBeNull]
|
[CanBeNull]
|
||||||
private IEditorChangeHandler changeHandler { get; set; }
|
private IEditorChangeHandler changeHandler { get; set; }
|
||||||
|
|
||||||
public SelectionEditablePath(Func<float, double> positionToDistance)
|
public SelectionEditablePath(Func<float, double> positionToTime)
|
||||||
: base(positionToDistance)
|
: base(positionToTime)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddVertex(Vector2 relativePosition)
|
public void AddVertex(Vector2 relativePosition)
|
||||||
{
|
{
|
||||||
double distance = Math.Max(0, PositionToDistance(relativePosition.Y));
|
double time = Math.Max(0, PositionToTime(relativePosition.Y));
|
||||||
int index = AddVertex(distance, relativePosition.X);
|
int index = AddVertex(time, relativePosition.X);
|
||||||
selectOnly(index);
|
selectOnly(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,9 +83,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
protected override void OnDrag(DragEvent e)
|
protected override void OnDrag(DragEvent e)
|
||||||
{
|
{
|
||||||
Vector2 mousePosition = ToRelativePosition(e.ScreenSpaceMousePosition);
|
Vector2 mousePosition = ToRelativePosition(e.ScreenSpaceMousePosition);
|
||||||
double distanceDelta = PositionToDistance(mousePosition.Y) - PositionToDistance(dragStartPosition.Y);
|
double timeDelta = PositionToTime(mousePosition.Y) - PositionToTime(dragStartPosition.Y);
|
||||||
float xDelta = mousePosition.X - dragStartPosition.X;
|
float xDelta = mousePosition.X - dragStartPosition.X;
|
||||||
MoveSelectedVertices(distanceDelta, xDelta);
|
MoveSelectedVertices(timeDelta, xDelta);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnDragEnd(DragEndEvent e)
|
protected override void OnDragEnd(DragEndEvent e)
|
||||||
|
@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
{
|
{
|
||||||
scrollingPath = new ScrollingPath(),
|
scrollingPath = new ScrollingPath(),
|
||||||
nestedOutlineContainer = new NestedOutlineContainer(),
|
nestedOutlineContainer = new NestedOutlineContainer(),
|
||||||
editablePath = new PlacementEditablePath(positionToDistance)
|
editablePath = new PlacementEditablePath(positionToTime)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,10 +121,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
lastEditablePathId = editablePath.PathId;
|
lastEditablePathId = editablePath.PathId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private double positionToDistance(float relativeYPosition)
|
private double positionToTime(float relativeYPosition)
|
||||||
{
|
{
|
||||||
double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime);
|
double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime);
|
||||||
return (time - HitObject.StartTime) * HitObject.Velocity;
|
return time - HitObject.StartTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
{
|
{
|
||||||
scrollingPath = new ScrollingPath(),
|
scrollingPath = new ScrollingPath(),
|
||||||
nestedOutlineContainer = new NestedOutlineContainer(),
|
nestedOutlineContainer = new NestedOutlineContainer(),
|
||||||
editablePath = new SelectionEditablePath(positionToDistance)
|
editablePath = new SelectionEditablePath(positionToTime)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,10 +145,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
return new RectangleF(left, top, right - left, bottom - top).Inflate(objectRadius);
|
return new RectangleF(left, top, right - left, bottom - top).Inflate(objectRadius);
|
||||||
}
|
}
|
||||||
|
|
||||||
private double positionToDistance(float relativeYPosition)
|
private double positionToTime(float relativeYPosition)
|
||||||
{
|
{
|
||||||
double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime);
|
double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime);
|
||||||
return (time - HitObject.StartTime) * HitObject.Velocity;
|
return time - HitObject.StartTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeJuiceStreamPath()
|
private void initializeJuiceStreamPath()
|
||||||
|
@ -6,6 +6,7 @@ using System.Linq;
|
|||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.EnumExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.Input;
|
using osu.Framework.Input;
|
||||||
@ -89,15 +90,19 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler })
|
new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler })
|
||||||
});
|
});
|
||||||
|
|
||||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
|
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||||
{
|
{
|
||||||
var result = base.FindSnappedPositionAndTime(screenSpacePosition);
|
var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
|
||||||
|
|
||||||
result.ScreenSpacePosition.X = screenSpacePosition.X;
|
result.ScreenSpacePosition.X = screenSpacePosition.X;
|
||||||
|
|
||||||
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
|
if (snapType.HasFlagFast(SnapType.Grids))
|
||||||
Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius)
|
|
||||||
{
|
{
|
||||||
result = snapResult;
|
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
|
||||||
|
Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius)
|
||||||
|
{
|
||||||
|
result = snapResult;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
@ -27,10 +27,16 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
public int RepeatCount { get; set; }
|
public int RepeatCount { get; set; }
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public double Velocity { get; private set; }
|
private double velocityFactor;
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public double TickDistance { get; private set; }
|
private double tickDistanceFactor;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public double Velocity => velocityFactor * DifficultyControlPoint.SliderVelocity;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public double TickDistance => tickDistanceFactor * DifficultyControlPoint.SliderVelocity;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The length of one span of this <see cref="JuiceStream"/>.
|
/// The length of one span of this <see cref="JuiceStream"/>.
|
||||||
@ -43,10 +49,8 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
|
|
||||||
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
|
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
|
||||||
|
|
||||||
double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
|
velocityFactor = base_scoring_distance * difficulty.SliderMultiplier / timingPoint.BeatLength;
|
||||||
|
tickDistanceFactor = base_scoring_distance * difficulty.SliderMultiplier / difficulty.SliderTickRate;
|
||||||
Velocity = scoringDistance / timingPoint.BeatLength;
|
|
||||||
TickDistance = scoringDistance / difficulty.SliderTickRate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||||
|
@ -20,11 +20,6 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
/// However, the <see cref="SliderPath"/> representation is difficult to work with.
|
/// However, the <see cref="SliderPath"/> representation is difficult to work with.
|
||||||
/// This <see cref="JuiceStreamPath"/> represents the path in a more convenient way, a polyline connecting list of <see cref="JuiceStreamPathVertex"/>s.
|
/// This <see cref="JuiceStreamPath"/> represents the path in a more convenient way, a polyline connecting list of <see cref="JuiceStreamPathVertex"/>s.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <para>
|
|
||||||
/// The path can be regarded as a function from the closed interval <c>[Vertices[0].Distance, Vertices[^1].Distance]</c> to the x position, given by <see cref="PositionAtDistance"/>.
|
|
||||||
/// To ensure the path is convertible to a <see cref="SliderPath"/>, the slope of the function must not be more than <c>1</c> everywhere,
|
|
||||||
/// and this slope condition is always maintained as an invariant.
|
|
||||||
/// </para>
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class JuiceStreamPath
|
public class JuiceStreamPath
|
||||||
{
|
{
|
||||||
@ -46,9 +41,9 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
public int InvalidationID { get; private set; } = 1;
|
public int InvalidationID { get; private set; } = 1;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The difference between first vertex's <see cref="JuiceStreamPathVertex.Distance"/> and last vertex's <see cref="JuiceStreamPathVertex.Distance"/>.
|
/// The difference between first vertex's <see cref="JuiceStreamPathVertex.Time"/> and last vertex's <see cref="JuiceStreamPathVertex.Time"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double Distance => vertices[^1].Distance - vertices[0].Distance;
|
public double Duration => vertices[^1].Time - vertices[0].Time;
|
||||||
|
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// This list should always be non-empty.
|
/// This list should always be non-empty.
|
||||||
@ -59,15 +54,15 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Compute the x-position of the path at the given <paramref name="distance"/>.
|
/// Compute the x-position of the path at the given <paramref name="time"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// When the given distance is outside of the path, the x position at the corresponding endpoint is returned,
|
/// When the given time is outside of the path, the x position at the corresponding endpoint is returned,
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public float PositionAtDistance(double distance)
|
public float PositionAtTime(double time)
|
||||||
{
|
{
|
||||||
int index = vertexIndexAtDistance(distance);
|
int index = vertexIndexAtTime(time);
|
||||||
return positionAtDistance(distance, index);
|
return positionAtTime(time, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -81,19 +76,19 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Insert a vertex at given <paramref name="distance"/>.
|
/// Insert a vertex at given <paramref name="time"/>.
|
||||||
/// The <see cref="PositionAtDistance"/> is used as the position of the new vertex.
|
/// The <see cref="PositionAtTime"/> is used as the position of the new vertex.
|
||||||
/// Thus, the set of points of the path is not changed (up to floating-point precision).
|
/// Thus, the set of points of the path is not changed (up to floating-point precision).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The index of the new vertex.</returns>
|
/// <returns>The index of the new vertex.</returns>
|
||||||
public int InsertVertex(double distance)
|
public int InsertVertex(double time)
|
||||||
{
|
{
|
||||||
if (!double.IsFinite(distance))
|
if (!double.IsFinite(time))
|
||||||
throw new ArgumentOutOfRangeException(nameof(distance));
|
throw new ArgumentOutOfRangeException(nameof(time));
|
||||||
|
|
||||||
int index = vertexIndexAtDistance(distance);
|
int index = vertexIndexAtTime(time);
|
||||||
float x = positionAtDistance(distance, index);
|
float x = positionAtTime(time, index);
|
||||||
vertices.Insert(index, new JuiceStreamPathVertex(distance, x));
|
vertices.Insert(index, new JuiceStreamPathVertex(time, x));
|
||||||
|
|
||||||
invalidate();
|
invalidate();
|
||||||
return index;
|
return index;
|
||||||
@ -101,7 +96,6 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Move the vertex of given <paramref name="index"/> to the given position <paramref name="newX"/>.
|
/// Move the vertex of given <paramref name="index"/> to the given position <paramref name="newX"/>.
|
||||||
/// When the distances between vertices are too small for the new vertex positions, the adjacent vertices are moved towards <paramref name="newX"/>.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void SetVertexPosition(int index, float newX)
|
public void SetVertexPosition(int index, float newX)
|
||||||
{
|
{
|
||||||
@ -111,32 +105,17 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
if (!float.IsFinite(newX))
|
if (!float.IsFinite(newX))
|
||||||
throw new ArgumentOutOfRangeException(nameof(newX));
|
throw new ArgumentOutOfRangeException(nameof(newX));
|
||||||
|
|
||||||
var newVertex = new JuiceStreamPathVertex(vertices[index].Distance, newX);
|
vertices[index] = new JuiceStreamPathVertex(vertices[index].Time, newX);
|
||||||
|
|
||||||
for (int i = index - 1; i >= 0 && !canConnect(vertices[i], newVertex); i--)
|
|
||||||
{
|
|
||||||
float clampedX = clampToConnectablePosition(newVertex, vertices[i]);
|
|
||||||
vertices[i] = new JuiceStreamPathVertex(vertices[i].Distance, clampedX);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = index + 1; i < vertices.Count; i++)
|
|
||||||
{
|
|
||||||
float clampedX = clampToConnectablePosition(newVertex, vertices[i]);
|
|
||||||
vertices[i] = new JuiceStreamPathVertex(vertices[i].Distance, clampedX);
|
|
||||||
}
|
|
||||||
|
|
||||||
vertices[index] = newVertex;
|
|
||||||
|
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add a new vertex at given <paramref name="distance"/> and position.
|
/// Add a new vertex at given <paramref name="time"/> and position.
|
||||||
/// Adjacent vertices are moved when necessary in the same way as <see cref="SetVertexPosition"/>.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Add(double distance, float x)
|
public void Add(double time, float x)
|
||||||
{
|
{
|
||||||
int index = InsertVertex(distance);
|
int index = InsertVertex(time);
|
||||||
SetVertexPosition(index, x);
|
SetVertexPosition(index, x);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,22 +142,22 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Recreate this path by using difference set of vertices at given distances.
|
/// Recreate this path by using difference set of vertices at given time points.
|
||||||
/// In addition to the given <paramref name="sampleDistances"/>, the first vertex and the last vertex are always added to the new path.
|
/// In addition to the given <paramref name="sampleTimes"/>, the first vertex and the last vertex are always added to the new path.
|
||||||
/// New vertices use the positions on the original path. Thus, <see cref="PositionAtDistance"/>s at <paramref name="sampleDistances"/> are preserved.
|
/// New vertices use the positions on the original path. Thus, <see cref="PositionAtTime"/>s at <paramref name="sampleTimes"/> are preserved.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void ResampleVertices(IEnumerable<double> sampleDistances)
|
public void ResampleVertices(IEnumerable<double> sampleTimes)
|
||||||
{
|
{
|
||||||
var sampledVertices = new List<JuiceStreamPathVertex>();
|
var sampledVertices = new List<JuiceStreamPathVertex>();
|
||||||
|
|
||||||
foreach (double distance in sampleDistances)
|
foreach (double time in sampleTimes)
|
||||||
{
|
{
|
||||||
if (!double.IsFinite(distance))
|
if (!double.IsFinite(time))
|
||||||
throw new ArgumentOutOfRangeException(nameof(sampleDistances));
|
throw new ArgumentOutOfRangeException(nameof(sampleTimes));
|
||||||
|
|
||||||
double clampedDistance = Math.Clamp(distance, vertices[0].Distance, vertices[^1].Distance);
|
double clampedTime = Math.Clamp(time, vertices[0].Time, vertices[^1].Time);
|
||||||
float x = PositionAtDistance(clampedDistance);
|
float x = PositionAtTime(clampedTime);
|
||||||
sampledVertices.Add(new JuiceStreamPathVertex(clampedDistance, x));
|
sampledVertices.Add(new JuiceStreamPathVertex(clampedTime, x));
|
||||||
}
|
}
|
||||||
|
|
||||||
sampledVertices.Sort();
|
sampledVertices.Sort();
|
||||||
@ -196,37 +175,62 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Duplicated vertices are automatically removed.
|
/// Duplicated vertices are automatically removed.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public void ConvertFromSliderPath(SliderPath sliderPath)
|
public void ConvertFromSliderPath(SliderPath sliderPath, double velocity)
|
||||||
{
|
{
|
||||||
var sliderPathVertices = new List<Vector2>();
|
var sliderPathVertices = new List<Vector2>();
|
||||||
sliderPath.GetPathToProgress(sliderPathVertices, 0, 1);
|
sliderPath.GetPathToProgress(sliderPathVertices, 0, 1);
|
||||||
|
|
||||||
double distance = 0;
|
double time = 0;
|
||||||
|
|
||||||
vertices.Clear();
|
vertices.Clear();
|
||||||
vertices.Add(new JuiceStreamPathVertex(0, sliderPathVertices.FirstOrDefault().X));
|
vertices.Add(new JuiceStreamPathVertex(0, sliderPathVertices.FirstOrDefault().X));
|
||||||
|
|
||||||
for (int i = 1; i < sliderPathVertices.Count; i++)
|
for (int i = 1; i < sliderPathVertices.Count; i++)
|
||||||
{
|
{
|
||||||
distance += Vector2.Distance(sliderPathVertices[i - 1], sliderPathVertices[i]);
|
time += Vector2.Distance(sliderPathVertices[i - 1], sliderPathVertices[i]) / velocity;
|
||||||
|
|
||||||
if (!Precision.AlmostEquals(vertices[^1].Distance, distance))
|
if (!Precision.AlmostEquals(vertices[^1].Time, time))
|
||||||
vertices.Add(new JuiceStreamPathVertex(distance, sliderPathVertices[i].X));
|
Add(time, sliderPathVertices[i].X);
|
||||||
}
|
}
|
||||||
|
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the minimum slider velocity required to convert this path to a <see cref="SliderPath"/>.
|
||||||
|
/// </summary>
|
||||||
|
public double ComputeRequiredVelocity()
|
||||||
|
{
|
||||||
|
double maximumSlope = 0;
|
||||||
|
|
||||||
|
for (int i = 1; i < vertices.Count; i++)
|
||||||
|
{
|
||||||
|
double xDifference = Math.Abs((double)vertices[i].X - vertices[i - 1].X);
|
||||||
|
double timeDifference = vertices[i].Time - vertices[i - 1].Time;
|
||||||
|
|
||||||
|
// A short segment won't affect the resulting path much anyways so ignore it to avoid divide-by-zero.
|
||||||
|
if (Precision.AlmostEquals(timeDifference, 0))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
maximumSlope = Math.Max(maximumSlope, xDifference / timeDifference);
|
||||||
|
}
|
||||||
|
|
||||||
|
return maximumSlope;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convert the path of this <see cref="JuiceStreamPath"/> to a <see cref="SliderPath"/> and write the result to <paramref name="sliderPath"/>.
|
/// Convert the path of this <see cref="JuiceStreamPath"/> to a <see cref="SliderPath"/> and write the result to <paramref name="sliderPath"/>.
|
||||||
/// The resulting slider is "folded" to make it vertically contained in the playfield `(0..<see cref="OSU_PLAYFIELD_HEIGHT"/>)` assuming the slider start position is <paramref name="sliderStartY"/>.
|
/// The resulting slider is "folded" to make it vertically contained in the playfield `(0..<see cref="OSU_PLAYFIELD_HEIGHT"/>)` assuming the slider start position is <paramref name="sliderStartY"/>.
|
||||||
|
///
|
||||||
|
/// The velocity of the converted slider is assumed to be <paramref name="velocity"/>.
|
||||||
|
/// To preserve the path, <paramref name="velocity"/> should be at least the value returned by <see cref="ComputeRequiredVelocity"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void ConvertToSliderPath(SliderPath sliderPath, float sliderStartY)
|
public void ConvertToSliderPath(SliderPath sliderPath, float sliderStartY, double velocity)
|
||||||
{
|
{
|
||||||
const float margin = 1;
|
const float margin = 1;
|
||||||
|
|
||||||
// Note: these two variables and `sliderPath` are modified by the local functions.
|
// Note: these two variables and `sliderPath` are modified by the local functions.
|
||||||
double currentDistance = 0;
|
double currentTime = 0;
|
||||||
Vector2 lastPosition = new Vector2(vertices[0].X, 0);
|
Vector2 lastPosition = new Vector2(vertices[0].X, 0);
|
||||||
|
|
||||||
sliderPath.ControlPoints.Clear();
|
sliderPath.ControlPoints.Clear();
|
||||||
@ -237,10 +241,10 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
sliderPath.ControlPoints[^1].Type = PathType.Linear;
|
sliderPath.ControlPoints[^1].Type = PathType.Linear;
|
||||||
|
|
||||||
float deltaX = vertices[i].X - lastPosition.X;
|
float deltaX = vertices[i].X - lastPosition.X;
|
||||||
double length = vertices[i].Distance - currentDistance;
|
double length = (vertices[i].Time - currentTime) * velocity;
|
||||||
|
|
||||||
// Should satisfy `deltaX^2 + deltaY^2 = length^2`.
|
// Should satisfy `deltaX^2 + deltaY^2 = length^2`.
|
||||||
// By invariants, the expression inside the `sqrt` is (almost) non-negative.
|
// The expression inside the `sqrt` is (almost) non-negative if the slider velocity is large enough.
|
||||||
double deltaY = Math.Sqrt(Math.Max(0, length * length - (double)deltaX * deltaX));
|
double deltaY = Math.Sqrt(Math.Max(0, length * length - (double)deltaX * deltaX));
|
||||||
|
|
||||||
// When `deltaY` is small, one segment is always enough.
|
// When `deltaY` is small, one segment is always enough.
|
||||||
@ -280,59 +284,38 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
{
|
{
|
||||||
Vector2 nextPosition = new Vector2(nextX, nextY);
|
Vector2 nextPosition = new Vector2(nextX, nextY);
|
||||||
sliderPath.ControlPoints.Add(new PathControlPoint(nextPosition));
|
sliderPath.ControlPoints.Add(new PathControlPoint(nextPosition));
|
||||||
currentDistance += Vector2.Distance(lastPosition, nextPosition);
|
currentTime += Vector2.Distance(lastPosition, nextPosition) / velocity;
|
||||||
lastPosition = nextPosition;
|
lastPosition = nextPosition;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Find the index at which a new vertex with <paramref name="distance"/> can be inserted.
|
/// Find the index at which a new vertex with <paramref name="time"/> can be inserted.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private int vertexIndexAtDistance(double distance)
|
private int vertexIndexAtTime(double time)
|
||||||
{
|
{
|
||||||
// The position of `(distance, Infinity)` is uniquely determined because infinite positions are not allowed.
|
// The position of `(time, Infinity)` is uniquely determined because infinite positions are not allowed.
|
||||||
int i = vertices.BinarySearch(new JuiceStreamPathVertex(distance, float.PositiveInfinity));
|
int i = vertices.BinarySearch(new JuiceStreamPathVertex(time, float.PositiveInfinity));
|
||||||
return i < 0 ? ~i : i;
|
return i < 0 ? ~i : i;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Compute the position at the given <paramref name="distance"/>, assuming <paramref name="index"/> is the vertex index returned by <see cref="vertexIndexAtDistance"/>.
|
/// Compute the position at the given <paramref name="time"/>, assuming <paramref name="index"/> is the vertex index returned by <see cref="vertexIndexAtTime"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private float positionAtDistance(double distance, int index)
|
private float positionAtTime(double time, int index)
|
||||||
{
|
{
|
||||||
if (index <= 0)
|
if (index <= 0)
|
||||||
return vertices[0].X;
|
return vertices[0].X;
|
||||||
if (index >= vertices.Count)
|
if (index >= vertices.Count)
|
||||||
return vertices[^1].X;
|
return vertices[^1].X;
|
||||||
|
|
||||||
double length = vertices[index].Distance - vertices[index - 1].Distance;
|
double duration = vertices[index].Time - vertices[index - 1].Time;
|
||||||
if (Precision.AlmostEquals(length, 0))
|
if (Precision.AlmostEquals(duration, 0))
|
||||||
return vertices[index].X;
|
return vertices[index].X;
|
||||||
|
|
||||||
float deltaX = vertices[index].X - vertices[index - 1].X;
|
float deltaX = vertices[index].X - vertices[index - 1].X;
|
||||||
|
|
||||||
return (float)(vertices[index - 1].X + deltaX * ((distance - vertices[index - 1].Distance) / length));
|
return (float)(vertices[index - 1].X + deltaX * ((time - vertices[index - 1].Time) / duration));
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Check the two vertices can connected directly while satisfying the slope condition.
|
|
||||||
/// </summary>
|
|
||||||
private bool canConnect(JuiceStreamPathVertex vertex1, JuiceStreamPathVertex vertex2, float allowance = 0)
|
|
||||||
{
|
|
||||||
double xDistance = Math.Abs((double)vertex2.X - vertex1.X);
|
|
||||||
float length = (float)Math.Abs(vertex2.Distance - vertex1.Distance);
|
|
||||||
return xDistance <= length + allowance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Move the position of <paramref name="movableVertex"/> towards the position of <paramref name="fixedVertex"/>
|
|
||||||
/// until the vertex pair satisfies the condition <see cref="canConnect"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The resulting position of <paramref name="movableVertex"/>.</returns>
|
|
||||||
private float clampToConnectablePosition(JuiceStreamPathVertex fixedVertex, JuiceStreamPathVertex movableVertex)
|
|
||||||
{
|
|
||||||
float length = (float)Math.Abs(movableVertex.Distance - fixedVertex.Distance);
|
|
||||||
return Math.Clamp(movableVertex.X, fixedVertex.X - length, fixedVertex.X + length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void invalidate() => InvalidationID++;
|
private void invalidate() => InvalidationID++;
|
||||||
|
@ -12,22 +12,22 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly struct JuiceStreamPathVertex : IComparable<JuiceStreamPathVertex>
|
public readonly struct JuiceStreamPathVertex : IComparable<JuiceStreamPathVertex>
|
||||||
{
|
{
|
||||||
public readonly double Distance;
|
public readonly double Time;
|
||||||
|
|
||||||
public readonly float X;
|
public readonly float X;
|
||||||
|
|
||||||
public JuiceStreamPathVertex(double distance, float x)
|
public JuiceStreamPathVertex(double time, float x)
|
||||||
{
|
{
|
||||||
Distance = distance;
|
Time = time;
|
||||||
X = x;
|
X = x;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int CompareTo(JuiceStreamPathVertex other)
|
public int CompareTo(JuiceStreamPathVertex other)
|
||||||
{
|
{
|
||||||
int c = Distance.CompareTo(other.Distance);
|
int c = Time.CompareTo(other.Time);
|
||||||
return c != 0 ? c : X.CompareTo(other.X);
|
return c != 0 ? c : X.CompareTo(other.X);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString() => $"({Distance}, {X})";
|
public override string ToString() => $"({Time}, {X})";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,8 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>XSAppIconAssets</key>
|
<key>XSAppIconAssets</key>
|
||||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||||
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
<true/>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
|
@ -97,12 +97,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
|||||||
set => InternalChild = value;
|
set => InternalChild = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
|
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||||
{
|
|
||||||
throw new System.NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override SnapResult FindSnappedPosition(Vector2 screenSpacePosition)
|
|
||||||
{
|
{
|
||||||
throw new System.NotImplementedException();
|
throw new System.NotImplementedException();
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,10 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Mania.Objects;
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
|
using osu.Game.Rulesets.Mania.Skinning.Default;
|
||||||
using osu.Game.Rulesets.Mania.UI;
|
using osu.Game.Rulesets.Mania.UI;
|
||||||
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
|
using osuTK;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||||
@ -52,8 +55,29 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
|||||||
{
|
{
|
||||||
base.UpdateTimeAndPosition(result);
|
base.UpdateTimeAndPosition(result);
|
||||||
|
|
||||||
if (PlacementActive == PlacementState.Waiting)
|
if (result.Playfield is Column col)
|
||||||
Column = result.Playfield as Column;
|
{
|
||||||
|
// Apply an offset to better align with the visual grid.
|
||||||
|
// This should only be applied during placement, as during selection / drag operations the movement is relative
|
||||||
|
// to the initial point of interaction rather than the grid.
|
||||||
|
switch (col.ScrollingInfo.Direction.Value)
|
||||||
|
{
|
||||||
|
case ScrollingDirection.Down:
|
||||||
|
result.ScreenSpacePosition -= new Vector2(0, getNoteHeight(col) / 2);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ScrollingDirection.Up:
|
||||||
|
result.ScreenSpacePosition += new Vector2(0, getNoteHeight(col) / 2);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PlacementActive == PlacementState.Waiting)
|
||||||
|
Column = col;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private float getNoteHeight(Column resultPlayfield) =>
|
||||||
|
resultPlayfield.ToScreenSpace(new Vector2(DefaultNotePiece.NOTE_HEIGHT)).Y -
|
||||||
|
resultPlayfield.ToScreenSpace(Vector2.Zero).Y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Game.Beatmaps;
|
|
||||||
using osu.Game.Rulesets.Edit;
|
|
||||||
using osu.Game.Rulesets.Edit.Tools;
|
|
||||||
using osu.Game.Rulesets.Mania.Objects;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Input;
|
using osu.Framework.Input;
|
||||||
using osu.Game.Rulesets.Mania.Skinning.Default;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osu.Game.Rulesets.Edit.Tools;
|
||||||
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
using osu.Game.Rulesets.Mania.UI;
|
using osu.Game.Rulesets.Mania.UI;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
@ -56,28 +55,6 @@ namespace osu.Game.Rulesets.Mania.Edit
|
|||||||
protected override Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) =>
|
protected override Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) =>
|
||||||
Playfield.GetColumnByPosition(screenSpacePosition);
|
Playfield.GetColumnByPosition(screenSpacePosition);
|
||||||
|
|
||||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
|
|
||||||
{
|
|
||||||
var result = base.FindSnappedPositionAndTime(screenSpacePosition);
|
|
||||||
|
|
||||||
switch (ScrollingInfo.Direction.Value)
|
|
||||||
{
|
|
||||||
case ScrollingDirection.Down:
|
|
||||||
result.ScreenSpacePosition -= new Vector2(0, getNoteHeight() / 2);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ScrollingDirection.Up:
|
|
||||||
result.ScreenSpacePosition += new Vector2(0, getNoteHeight() / 2);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private float getNoteHeight() =>
|
|
||||||
Playfield.GetColumn(0).ToScreenSpace(new Vector2(DefaultNotePiece.NOTE_HEIGHT)).Y -
|
|
||||||
Playfield.GetColumn(0).ToScreenSpace(Vector2.Zero).Y;
|
|
||||||
|
|
||||||
protected override DrawableRuleset<ManiaHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
protected override DrawableRuleset<ManiaHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
||||||
{
|
{
|
||||||
drawableRuleset = new DrawableManiaEditorRuleset(ruleset, beatmap, mods);
|
drawableRuleset = new DrawableManiaEditorRuleset(ruleset, beatmap, mods);
|
||||||
|
@ -37,6 +37,8 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>XSAppIconAssets</key>
|
<key>XSAppIconAssets</key>
|
||||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||||
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
<true/>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
|
@ -7,6 +7,7 @@ using System.Linq;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Caching;
|
using osu.Framework.Caching;
|
||||||
|
using osu.Framework.Extensions.EnumExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
@ -123,33 +124,27 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override SnapResult FindSnappedPosition(Vector2 screenSpacePosition)
|
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||||
{
|
{
|
||||||
if (snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
|
if (snapType.HasFlagFast(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
|
||||||
return snapResult;
|
return snapResult;
|
||||||
|
|
||||||
return new SnapResult(screenSpacePosition, null);
|
if (snapType.HasFlagFast(SnapType.Grids))
|
||||||
}
|
|
||||||
|
|
||||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
|
|
||||||
{
|
|
||||||
var positionSnap = FindSnappedPosition(screenSpacePosition);
|
|
||||||
if (positionSnap.ScreenSpacePosition != screenSpacePosition)
|
|
||||||
return positionSnap;
|
|
||||||
|
|
||||||
if (distanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
|
|
||||||
{
|
{
|
||||||
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
|
if (distanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
|
||||||
return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition));
|
{
|
||||||
|
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
|
||||||
|
return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rectangularGridSnapToggle.Value == TernaryState.True)
|
||||||
|
{
|
||||||
|
Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(screenSpacePosition));
|
||||||
|
return new SnapResult(rectangularPositionSnapGrid.ToScreenSpace(pos), null, PlayfieldAtScreenSpacePosition(screenSpacePosition));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rectangularGridSnapToggle.Value == TernaryState.True)
|
return base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
|
||||||
{
|
|
||||||
Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(screenSpacePosition));
|
|
||||||
return new SnapResult(rectangularPositionSnapGrid.ToScreenSpace(pos), null, PlayfieldAtScreenSpacePosition(screenSpacePosition));
|
|
||||||
}
|
|
||||||
|
|
||||||
return base.FindSnappedPositionAndTime(screenSpacePosition);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult)
|
private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult)
|
||||||
|
@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
private readonly Bindable<Color4> accentColour = new Bindable<Color4>();
|
private readonly Bindable<Color4> accentColour = new Bindable<Color4>();
|
||||||
private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>();
|
private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>();
|
||||||
|
|
||||||
[Resolved(canBeNull: true)]
|
[Resolved(canBeNull: true)] // Can't really be null but required to handle potential of disposal before DI completes.
|
||||||
private DrawableHitObject? drawableObject { get; set; }
|
private DrawableHitObject? drawableObject { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
|
@ -37,6 +37,8 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>XSAppIconAssets</key>
|
<key>XSAppIconAssets</key>
|
||||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||||
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
<true/>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
|
@ -37,6 +37,8 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>XSAppIconAssets</key>
|
<key>XSAppIconAssets</key>
|
||||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||||
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
<true/>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
|
@ -863,5 +863,40 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
Assert.That(decoded.Difficulty.OverallDifficulty, Is.EqualTo(1));
|
Assert.That(decoded.Difficulty.OverallDifficulty, Is.EqualTo(1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestLegacyAdjacentImplicitCatmullSegmentsAreMerged()
|
||||||
|
{
|
||||||
|
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
|
||||||
|
|
||||||
|
using (var resStream = TestResources.OpenResource("adjacent-catmull-segments.osu"))
|
||||||
|
using (var stream = new LineBufferedReader(resStream))
|
||||||
|
{
|
||||||
|
var decoded = decoder.Decode(stream);
|
||||||
|
var controlPoints = ((IHasPath)decoded.HitObjects[0]).Path.ControlPoints;
|
||||||
|
|
||||||
|
Assert.That(controlPoints.Count, Is.EqualTo(6));
|
||||||
|
Assert.That(controlPoints.Single(c => c.Type != null).Type, Is.EqualTo(PathType.Catmull));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNonLegacyAdjacentImplicitCatmullSegmentsAreNotMerged()
|
||||||
|
{
|
||||||
|
var decoder = new LegacyBeatmapDecoder(LegacyBeatmapEncoder.FIRST_LAZER_VERSION) { ApplyOffsets = false };
|
||||||
|
|
||||||
|
using (var resStream = TestResources.OpenResource("adjacent-catmull-segments.osu"))
|
||||||
|
using (var stream = new LineBufferedReader(resStream))
|
||||||
|
{
|
||||||
|
var decoded = decoder.Decode(stream);
|
||||||
|
var controlPoints = ((IHasPath)decoded.HitObjects[0]).Path.ControlPoints;
|
||||||
|
|
||||||
|
Assert.That(controlPoints.Count, Is.EqualTo(4));
|
||||||
|
Assert.That(controlPoints[0].Type, Is.EqualTo(PathType.Catmull));
|
||||||
|
Assert.That(controlPoints[1].Type, Is.EqualTo(PathType.Catmull));
|
||||||
|
Assert.That(controlPoints[2].Type, Is.EqualTo(PathType.Catmull));
|
||||||
|
Assert.That(controlPoints[3].Type, Is.Null);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -507,7 +507,7 @@ namespace osu.Game.Tests.Database
|
|||||||
using (var stream = storage.GetStream(firstFile.File.GetStoragePath()))
|
using (var stream = storage.GetStream(firstFile.File.GetStoragePath()))
|
||||||
originalLength = stream.Length;
|
originalLength = stream.Length;
|
||||||
|
|
||||||
using (var stream = storage.GetStream(firstFile.File.GetStoragePath(), FileAccess.Write, FileMode.Create))
|
using (var stream = storage.CreateFileSafely(firstFile.File.GetStoragePath()))
|
||||||
stream.WriteByte(0);
|
stream.WriteByte(0);
|
||||||
|
|
||||||
var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm);
|
var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm);
|
||||||
|
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
|
private class SnapProvider : IDistanceSnapProvider
|
||||||
{
|
{
|
||||||
public SnapResult FindSnappedPosition(Vector2 screenSpacePosition) =>
|
public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.Grids) => new SnapResult(screenSpacePosition, 0);
|
||||||
new SnapResult(screenSpacePosition, null);
|
|
||||||
|
|
||||||
public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
|
|
||||||
|
|
||||||
public Bindable<double> DistanceSpacingMultiplier { get; } = new BindableDouble(1);
|
public Bindable<double> DistanceSpacingMultiplier { get; } = new BindableDouble(1);
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
@ -25,7 +24,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
if (isDisposing)
|
if (isDisposing)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
using (var outStream = LocalStorage.GetStream(DatabaseContextFactory.DATABASE_NAME, FileAccess.Write, FileMode.Create))
|
using (var outStream = LocalStorage.CreateFileSafely(DatabaseContextFactory.DATABASE_NAME))
|
||||||
using (var stream = TestResources.OpenResource(DatabaseContextFactory.DATABASE_NAME))
|
using (var stream = TestResources.OpenResource(DatabaseContextFactory.DATABASE_NAME))
|
||||||
stream.CopyTo(outStream);
|
stream.CopyTo(outStream);
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,8 @@ using System;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Threading;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
@ -31,7 +33,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class TestSceneChatOverlayV2 : OsuManualInputManagerTestScene
|
public class TestSceneChatOverlayV2 : OsuManualInputManagerTestScene
|
||||||
{
|
{
|
||||||
private ChatOverlayV2 chatOverlay;
|
private TestChatOverlayV2 chatOverlay;
|
||||||
private ChannelManager channelManager;
|
private ChannelManager channelManager;
|
||||||
|
|
||||||
private APIUser testUser;
|
private APIUser testUser;
|
||||||
@ -61,7 +63,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
channelManager,
|
channelManager,
|
||||||
chatOverlay = new ChatOverlayV2 { RelativeSizeAxes = Axes.Both },
|
chatOverlay = new TestChatOverlayV2 { RelativeSizeAxes = Axes.Both },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -365,19 +367,19 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TextBoxRetainsFocus()
|
public void TestTextBoxRetainsFocus()
|
||||||
{
|
{
|
||||||
AddStep("Show overlay", () => chatOverlay.Show());
|
AddStep("Show overlay", () => chatOverlay.Show());
|
||||||
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
||||||
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
|
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
|
||||||
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
|
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
|
||||||
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
||||||
|
AddStep("Click drawable channel", () => clickDrawable(currentDrawableChannel));
|
||||||
|
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
||||||
AddStep("Click selector", () => clickDrawable(channelSelectorButton));
|
AddStep("Click selector", () => clickDrawable(channelSelectorButton));
|
||||||
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
||||||
AddStep("Click listing", () => clickDrawable(chatOverlay.ChildrenOfType<ChannelListing>().Single()));
|
AddStep("Click listing", () => clickDrawable(chatOverlay.ChildrenOfType<ChannelListing>().Single()));
|
||||||
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
||||||
AddStep("Click drawable channel", () => clickDrawable(currentDrawableChannel));
|
|
||||||
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
|
||||||
AddStep("Click channel list", () => clickDrawable(chatOverlay.ChildrenOfType<ChannelList>().Single()));
|
AddStep("Click channel list", () => clickDrawable(chatOverlay.ChildrenOfType<ChannelList>().Single()));
|
||||||
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
||||||
AddStep("Click top bar", () => clickDrawable(chatOverlay.ChildrenOfType<ChatOverlayTopBar>().Single()));
|
AddStep("Click top bar", () => clickDrawable(chatOverlay.ChildrenOfType<ChatOverlayTopBar>().Single()));
|
||||||
@ -386,6 +388,34 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
AddAssert("TextBox is not focused", () => InputManager.FocusedDrawable == null);
|
AddAssert("TextBox is not focused", () => InputManager.FocusedDrawable == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSlowLoadingChannel()
|
||||||
|
{
|
||||||
|
AddStep("Show overlay (slow-loading)", () =>
|
||||||
|
{
|
||||||
|
chatOverlay.Show();
|
||||||
|
chatOverlay.SlowLoading = true;
|
||||||
|
});
|
||||||
|
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
|
||||||
|
AddAssert("Channel 1 loading", () => !channelIsVisible && chatOverlay.GetSlowLoadingChannel(testChannel1).LoadState == LoadState.Loading);
|
||||||
|
|
||||||
|
AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2));
|
||||||
|
AddStep("Select channel 2", () => clickDrawable(getChannelListItem(testChannel2)));
|
||||||
|
AddAssert("Channel 2 loading", () => !channelIsVisible && chatOverlay.GetSlowLoadingChannel(testChannel2).LoadState == LoadState.Loading);
|
||||||
|
|
||||||
|
AddStep("Finish channel 1 load", () => chatOverlay.GetSlowLoadingChannel(testChannel1).LoadEvent.Set());
|
||||||
|
AddAssert("Channel 1 ready", () => chatOverlay.GetSlowLoadingChannel(testChannel1).LoadState == LoadState.Ready);
|
||||||
|
AddAssert("Channel 1 not displayed", () => !channelIsVisible);
|
||||||
|
|
||||||
|
AddStep("Finish channel 2 load", () => chatOverlay.GetSlowLoadingChannel(testChannel2).LoadEvent.Set());
|
||||||
|
AddAssert("Channel 2 loaded", () => chatOverlay.GetSlowLoadingChannel(testChannel2).IsLoaded);
|
||||||
|
AddAssert("Channel 2 displayed", () => channelIsVisible && currentDrawableChannel.Channel == testChannel2);
|
||||||
|
|
||||||
|
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
|
||||||
|
AddAssert("Channel 1 loaded", () => chatOverlay.GetSlowLoadingChannel(testChannel1).IsLoaded);
|
||||||
|
AddAssert("Channel 1 displayed", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
|
||||||
|
}
|
||||||
|
|
||||||
private bool listingIsVisible =>
|
private bool listingIsVisible =>
|
||||||
chatOverlay.ChildrenOfType<ChannelListing>().Single().State.Value == Visibility.Visible;
|
chatOverlay.ChildrenOfType<ChannelListing>().Single().State.Value == Visibility.Visible;
|
||||||
|
|
||||||
@ -435,5 +465,35 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
Topic = $"We talk about the number {id} here",
|
Topic = $"We talk about the number {id} here",
|
||||||
Type = ChannelType.Public,
|
Type = ChannelType.Public,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private class TestChatOverlayV2 : ChatOverlayV2
|
||||||
|
{
|
||||||
|
public bool SlowLoading { get; set; }
|
||||||
|
|
||||||
|
public SlowLoadingDrawableChannel GetSlowLoadingChannel(Channel channel) => DrawableChannels.OfType<SlowLoadingDrawableChannel>().Single(c => c.Channel == channel);
|
||||||
|
|
||||||
|
protected override ChatOverlayDrawableChannel CreateDrawableChannel(Channel newChannel)
|
||||||
|
{
|
||||||
|
return SlowLoading
|
||||||
|
? new SlowLoadingDrawableChannel(newChannel)
|
||||||
|
: new ChatOverlayDrawableChannel(newChannel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SlowLoadingDrawableChannel : ChatOverlayDrawableChannel
|
||||||
|
{
|
||||||
|
public readonly ManualResetEventSlim LoadEvent = new ManualResetEventSlim();
|
||||||
|
|
||||||
|
public SlowLoadingDrawableChannel([NotNull] Channel channel)
|
||||||
|
: base(channel)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
LoadEvent.Wait(10000);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,7 +55,7 @@ namespace osu.Game.Tests.Visual.Settings
|
|||||||
.OfType<ISettingsItem>()
|
.OfType<ISettingsItem>()
|
||||||
.OfType<IFilterable>()
|
.OfType<IFilterable>()
|
||||||
.Where(f => !(f is IHasFilterableChildren))
|
.Where(f => !(f is IHasFilterableChildren))
|
||||||
.All(f => f.FilterTerms.Any(t => t.Contains("scaling")))
|
.All(f => f.FilterTerms.Any(t => t.ToString().Contains("scaling")))
|
||||||
));
|
));
|
||||||
|
|
||||||
AddAssert("ensure section is current", () => settings.CurrentSection.Value is GraphicsSection);
|
AddAssert("ensure section is current", () => settings.CurrentSection.Value is GraphicsSection);
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
@ -127,6 +126,12 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
AddAssert("check info labels count", () => infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Count() == expectedCount);
|
AddAssert("check info labels count", () => infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Count() == expectedCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SetUpSteps]
|
||||||
|
public void SetUpSteps()
|
||||||
|
{
|
||||||
|
AddStep("reset mods", () => SelectedMods.SetDefault());
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestNullBeatmap()
|
public void TestNullBeatmap()
|
||||||
{
|
{
|
||||||
@ -147,24 +152,48 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestBPMUpdates()
|
public void TestBPMUpdates()
|
||||||
{
|
{
|
||||||
const float bpm = 120;
|
const double bpm = 120;
|
||||||
IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo);
|
IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo);
|
||||||
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / bpm });
|
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / bpm });
|
||||||
|
|
||||||
OsuModDoubleTime doubleTime = null;
|
OsuModDoubleTime doubleTime = null;
|
||||||
|
|
||||||
selectBeatmap(beatmap);
|
selectBeatmap(beatmap);
|
||||||
checkDisplayedBPM(bpm);
|
checkDisplayedBPM($"{bpm}");
|
||||||
|
|
||||||
AddStep("select DT", () => SelectedMods.Value = new[] { doubleTime = new OsuModDoubleTime() });
|
AddStep("select DT", () => SelectedMods.Value = new[] { doubleTime = new OsuModDoubleTime() });
|
||||||
checkDisplayedBPM(bpm * 1.5f);
|
checkDisplayedBPM($"{bpm * 1.5f}");
|
||||||
|
|
||||||
AddStep("change DT rate", () => doubleTime.SpeedChange.Value = 2);
|
AddStep("change DT rate", () => doubleTime.SpeedChange.Value = 2);
|
||||||
checkDisplayedBPM(bpm * 2);
|
checkDisplayedBPM($"{bpm * 2}");
|
||||||
|
}
|
||||||
|
|
||||||
void checkDisplayedBPM(float target) =>
|
[TestCase(120, 125, null, "120-125 (mostly 120)")]
|
||||||
AddUntilStep($"displayed bpm is {target}", () => this.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Any(
|
[TestCase(120, 120.6, null, "120-121 (mostly 120)")]
|
||||||
label => label.Statistic.Name == "BPM" && label.Statistic.Content == target.ToString(CultureInfo.InvariantCulture)));
|
[TestCase(120, 120.4, null, "120")]
|
||||||
|
[TestCase(120, 120.6, "DT", "180-182 (mostly 180)")]
|
||||||
|
[TestCase(120, 120.4, "DT", "180")]
|
||||||
|
public void TestVaryingBPM(double commonBpm, double otherBpm, string mod, string expectedDisplay)
|
||||||
|
{
|
||||||
|
IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo);
|
||||||
|
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm });
|
||||||
|
beatmap.ControlPointInfo.Add(100, new TimingControlPoint { BeatLength = 60 * 1000 / otherBpm });
|
||||||
|
beatmap.ControlPointInfo.Add(200, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm });
|
||||||
|
|
||||||
|
if (mod != null)
|
||||||
|
AddStep($"select {mod}", () => SelectedMods.Value = new[] { Ruleset.Value.CreateInstance().CreateModFromAcronym(mod) });
|
||||||
|
|
||||||
|
selectBeatmap(beatmap);
|
||||||
|
checkDisplayedBPM(expectedDisplay);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkDisplayedBPM(string target)
|
||||||
|
{
|
||||||
|
AddUntilStep($"displayed bpm is {target}", () =>
|
||||||
|
{
|
||||||
|
var label = infoWedge.DisplayedContent.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Single(l => l.Statistic.Name == "BPM");
|
||||||
|
return label.Statistic.Content == target;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setRuleset(RulesetInfo rulesetInfo)
|
private void setRuleset(RulesetInfo rulesetInfo)
|
||||||
|
@ -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())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
AddAssert("results filtered correctly",
|
AddAssert("results filtered correctly",
|
||||||
() => playlistOverlay.ChildrenOfType<PlaylistItem>()
|
() => playlistOverlay.ChildrenOfType<PlaylistItem>()
|
||||||
.Where(item => item.MatchingFilter)
|
.Where(item => item.MatchingFilter)
|
||||||
.All(item => item.FilterTerms.Any(term => term.Contains("10"))));
|
.All(item => item.FilterTerms.Any(term => term.ToString().Contains("10"))));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ namespace osu.Game.Tournament.Tests.NonVisual
|
|||||||
// ReSharper disable once AccessToDisposedClosure
|
// ReSharper disable once AccessToDisposedClosure
|
||||||
var storage = host.Storage.GetStorageForDirectory(Path.Combine("tournaments", "default"));
|
var storage = host.Storage.GetStorageForDirectory(Path.Combine("tournaments", "default"));
|
||||||
|
|
||||||
using (var stream = storage.GetStream("bracket.json", FileAccess.Write, FileMode.Create))
|
using (var stream = storage.CreateFileSafely("bracket.json"))
|
||||||
using (var writer = new StreamWriter(stream))
|
using (var writer = new StreamWriter(stream))
|
||||||
{
|
{
|
||||||
writer.Write(@"{
|
writer.Write(@"{
|
||||||
|
@ -15,7 +15,7 @@ namespace osu.Game.Tournament.Tests.Screens
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(Storage storage)
|
private void load(Storage storage)
|
||||||
{
|
{
|
||||||
using (var stream = storage.GetStream("drawings.txt", FileAccess.Write))
|
using (var stream = storage.CreateFileSafely("drawings.txt"))
|
||||||
using (var writer = new StreamWriter(stream))
|
using (var writer = new StreamWriter(stream))
|
||||||
{
|
{
|
||||||
writer.WriteLine("KR : South Korea : KOR");
|
writer.WriteLine("KR : South Korea : KOR");
|
||||||
|
@ -45,7 +45,7 @@ namespace osu.Game.Tournament.Models
|
|||||||
|
|
||||||
public void SaveChanges()
|
public void SaveChanges()
|
||||||
{
|
{
|
||||||
using (var stream = configStorage.GetStream(config_path, FileAccess.Write, FileMode.Create))
|
using (var stream = configStorage.CreateFileSafely(config_path))
|
||||||
using (var sw = new StreamWriter(stream))
|
using (var sw = new StreamWriter(stream))
|
||||||
{
|
{
|
||||||
sw.Write(JsonConvert.SerializeObject(this,
|
sw.Write(JsonConvert.SerializeObject(this,
|
||||||
|
@ -205,7 +205,7 @@ namespace osu.Game.Tournament.Screens.Drawings
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Write to drawings_results
|
// Write to drawings_results
|
||||||
using (Stream stream = storage.GetStream(results_filename, FileAccess.Write, FileMode.Create))
|
using (Stream stream = storage.CreateFileSafely(results_filename))
|
||||||
using (StreamWriter sw = new StreamWriter(stream))
|
using (StreamWriter sw = new StreamWriter(stream))
|
||||||
{
|
{
|
||||||
sw.Write(text);
|
sw.Write(text);
|
||||||
|
@ -321,7 +321,7 @@ namespace osu.Game.Tournament
|
|||||||
Converters = new JsonConverter[] { new JsonPointConverter() }
|
Converters = new JsonConverter[] { new JsonPointConverter() }
|
||||||
});
|
});
|
||||||
|
|
||||||
using (var stream = storage.GetStream(BRACKET_FILENAME, FileAccess.Write, FileMode.Create))
|
using (var stream = storage.CreateFileSafely(BRACKET_FILENAME))
|
||||||
using (var sw = new StreamWriter(stream))
|
using (var sw = new StreamWriter(stream))
|
||||||
sw.Write(serialisedLadder);
|
sw.Write(serialisedLadder);
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,11 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.Online;
|
using osu.Game.Online;
|
||||||
@ -89,6 +91,8 @@ namespace osu.Game.Beatmaps.Drawables
|
|||||||
|
|
||||||
private void queueDownloads(string[] sourceFilenames, int? limit = null)
|
private void queueDownloads(string[] sourceFilenames, int? limit = null)
|
||||||
{
|
{
|
||||||
|
Debug.Assert(LoadState == LoadState.NotLoaded);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Matches osu-stable, in order to provide new users with roughly the same randomised selection of bundled beatmaps.
|
// Matches osu-stable, in order to provide new users with roughly the same randomised selection of bundled beatmaps.
|
||||||
|
@ -22,7 +22,7 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
{
|
{
|
||||||
public class LegacyBeatmapEncoder
|
public class LegacyBeatmapEncoder
|
||||||
{
|
{
|
||||||
public const int LATEST_VERSION = 128;
|
public const int FIRST_LAZER_VERSION = 128;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// osu! is generally slower than taiko, so a factor is added to increase
|
/// osu! is generally slower than taiko, so a factor is added to increase
|
||||||
@ -55,7 +55,7 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
|
|
||||||
public void Encode(TextWriter writer)
|
public void Encode(TextWriter writer)
|
||||||
{
|
{
|
||||||
writer.WriteLine($"osu file format v{LATEST_VERSION}");
|
writer.WriteLine($"osu file format v{FIRST_LAZER_VERSION}");
|
||||||
|
|
||||||
writer.WriteLine();
|
writer.WriteLine();
|
||||||
handleGeneral(writer);
|
handleGeneral(writer);
|
||||||
|
@ -111,6 +111,18 @@ namespace osu.Game.Collections
|
|||||||
|
|
||||||
public Action<Notification> PostNotification { protected get; set; }
|
public Action<Notification> PostNotification { protected get; set; }
|
||||||
|
|
||||||
|
public Task<int> GetAvailableCount(StableStorage stableStorage)
|
||||||
|
{
|
||||||
|
if (!stableStorage.Exists(database_name))
|
||||||
|
return Task.FromResult(0);
|
||||||
|
|
||||||
|
return Task.Run(() =>
|
||||||
|
{
|
||||||
|
using (var stream = stableStorage.GetStream(database_name))
|
||||||
|
return readCollections(stream).Count;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future.
|
/// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -37,7 +37,7 @@ namespace osu.Game.Database
|
|||||||
{
|
{
|
||||||
string filename = $"{item.GetDisplayString().GetValidArchiveContentFilename()}{FileExtension}";
|
string filename = $"{item.GetDisplayString().GetValidArchiveContentFilename()}{FileExtension}";
|
||||||
|
|
||||||
using (var stream = exportStorage.GetStream(filename, FileAccess.Write, FileMode.Create))
|
using (var stream = exportStorage.CreateFileSafely(filename))
|
||||||
ExportModelTo(item, stream);
|
ExportModelTo(item, stream);
|
||||||
|
|
||||||
exportStorage.PresentFileExternally(filename);
|
exportStorage.PresentFileExternally(filename);
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using osu.Framework;
|
using osu.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
@ -36,22 +37,66 @@ namespace osu.Game.Database
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private CollectionManager collections { get; set; }
|
private CollectionManager collections { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved(canBeNull: true)]
|
||||||
private OsuGame game { get; set; }
|
private OsuGame game { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private IDialogOverlay dialogOverlay { get; set; }
|
private IDialogOverlay dialogOverlay { get; set; }
|
||||||
|
|
||||||
[Resolved(CanBeNull = true)]
|
[Resolved(canBeNull: true)]
|
||||||
private DesktopGameHost desktopGameHost { get; set; }
|
private DesktopGameHost desktopGameHost { get; set; }
|
||||||
|
|
||||||
private StableStorage cachedStorage;
|
private StableStorage cachedStorage;
|
||||||
|
|
||||||
public bool SupportsImportFromStable => RuntimeInfo.IsDesktop;
|
public bool SupportsImportFromStable => RuntimeInfo.IsDesktop;
|
||||||
|
|
||||||
public async Task ImportFromStableAsync(StableContent content)
|
public void UpdateStorage(string stablePath) => cachedStorage = new StableStorage(stablePath, desktopGameHost);
|
||||||
|
|
||||||
|
public virtual async Task<int> GetImportCount(StableContent content, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var stableStorage = await getStableStorage().ConfigureAwait(false);
|
var stableStorage = GetCurrentStableStorage();
|
||||||
|
|
||||||
|
if (stableStorage == null)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
switch (content)
|
||||||
|
{
|
||||||
|
case StableContent.Beatmaps:
|
||||||
|
return await new LegacyBeatmapImporter(beatmaps).GetAvailableCount(stableStorage);
|
||||||
|
|
||||||
|
case StableContent.Skins:
|
||||||
|
return await new LegacySkinImporter(skins).GetAvailableCount(stableStorage);
|
||||||
|
|
||||||
|
case StableContent.Collections:
|
||||||
|
return await collections.GetAvailableCount(stableStorage);
|
||||||
|
|
||||||
|
case StableContent.Scores:
|
||||||
|
return await new LegacyScoreImporter(scores).GetAvailableCount(stableStorage);
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new ArgumentException($"Only one {nameof(StableContent)} flag should be specified.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ImportFromStableAsync(StableContent content, bool interactiveLocateIfNotFound = true)
|
||||||
|
{
|
||||||
|
var stableStorage = GetCurrentStableStorage();
|
||||||
|
|
||||||
|
if (stableStorage == null)
|
||||||
|
{
|
||||||
|
if (!interactiveLocateIfNotFound)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var taskCompletionSource = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
Schedule(() => dialogOverlay.Push(new StableDirectoryLocationDialog(taskCompletionSource)));
|
||||||
|
string stablePath = await taskCompletionSource.Task.ConfigureAwait(false);
|
||||||
|
|
||||||
|
UpdateStorage(stablePath);
|
||||||
|
stableStorage = GetCurrentStableStorage();
|
||||||
|
}
|
||||||
|
|
||||||
var importTasks = new List<Task>();
|
var importTasks = new List<Task>();
|
||||||
|
|
||||||
Task beatmapImportTask = Task.CompletedTask;
|
Task beatmapImportTask = Task.CompletedTask;
|
||||||
@ -70,20 +115,16 @@ namespace osu.Game.Database
|
|||||||
await Task.WhenAll(importTasks.ToArray()).ConfigureAwait(false);
|
await Task.WhenAll(importTasks.ToArray()).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<StableStorage> getStableStorage()
|
public StableStorage GetCurrentStableStorage()
|
||||||
{
|
{
|
||||||
if (cachedStorage != null)
|
if (cachedStorage != null)
|
||||||
return cachedStorage;
|
return cachedStorage;
|
||||||
|
|
||||||
var stableStorage = game.GetStorageForStableInstall();
|
var stableStorage = game?.GetStorageForStableInstall();
|
||||||
if (stableStorage != null)
|
if (stableStorage != null)
|
||||||
return cachedStorage = stableStorage;
|
return cachedStorage = stableStorage;
|
||||||
|
|
||||||
var taskCompletionSource = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
|
return null;
|
||||||
Schedule(() => dialogOverlay.Push(new StableDirectoryLocationDialog(taskCompletionSource)));
|
|
||||||
string stablePath = await taskCompletionSource.Task.ConfigureAwait(false);
|
|
||||||
|
|
||||||
return cachedStorage = new StableStorage(stablePath, desktopGameHost);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,8 +24,14 @@ namespace osu.Game.Database
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Select paths to import from stable where all paths should be absolute. Default implementation iterates all directories in <see cref="ImportFromStablePath"/>.
|
/// Select paths to import from stable where all paths should be absolute. Default implementation iterates all directories in <see cref="ImportFromStablePath"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual IEnumerable<string> GetStableImportPaths(Storage storage) => storage.GetDirectories(ImportFromStablePath)
|
protected virtual IEnumerable<string> GetStableImportPaths(Storage storage)
|
||||||
.Select(path => storage.GetFullPath(path));
|
{
|
||||||
|
if (!storage.ExistsDirectory(ImportFromStablePath))
|
||||||
|
return Enumerable.Empty<string>();
|
||||||
|
|
||||||
|
return storage.GetDirectories(ImportFromStablePath)
|
||||||
|
.Select(path => storage.GetFullPath(path));
|
||||||
|
}
|
||||||
|
|
||||||
protected readonly IModelImporter<TModel> Importer;
|
protected readonly IModelImporter<TModel> Importer;
|
||||||
|
|
||||||
@ -34,6 +40,8 @@ namespace osu.Game.Database
|
|||||||
Importer = importer;
|
Importer = importer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<int> GetAvailableCount(StableStorage stableStorage) => Task.Run(() => GetStableImportPaths(PrepareStableStorage(stableStorage)).Count());
|
||||||
|
|
||||||
public Task ImportFromStableAsync(StableStorage stableStorage)
|
public Task ImportFromStableAsync(StableStorage stableStorage)
|
||||||
{
|
{
|
||||||
var storage = PrepareStableStorage(stableStorage);
|
var storage = PrepareStableStorage(stableStorage);
|
||||||
|
@ -15,8 +15,14 @@ namespace osu.Game.Database
|
|||||||
protected override string ImportFromStablePath => Path.Combine("Data", "r");
|
protected override string ImportFromStablePath => Path.Combine("Data", "r");
|
||||||
|
|
||||||
protected override IEnumerable<string> GetStableImportPaths(Storage storage)
|
protected override IEnumerable<string> GetStableImportPaths(Storage storage)
|
||||||
=> storage.GetFiles(ImportFromStablePath).Where(p => Importer.HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false))
|
{
|
||||||
.Select(path => storage.GetFullPath(path));
|
if (!storage.ExistsDirectory(ImportFromStablePath))
|
||||||
|
return Enumerable.Empty<string>();
|
||||||
|
|
||||||
|
return storage.GetFiles(ImportFromStablePath)
|
||||||
|
.Where(p => Importer.HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false))
|
||||||
|
.Select(path => storage.GetFullPath(path));
|
||||||
|
}
|
||||||
|
|
||||||
public LegacyScoreImporter(IModelImporter<ScoreInfo> importer)
|
public LegacyScoreImporter(IModelImporter<ScoreInfo> importer)
|
||||||
: base(importer)
|
: base(importer)
|
||||||
|
@ -242,7 +242,7 @@ namespace osu.Game.Database
|
|||||||
storage.Delete(Filename);
|
storage.Delete(Filename);
|
||||||
|
|
||||||
using (var inputStream = storage.GetStream(recoveryFilename))
|
using (var inputStream = storage.GetStream(recoveryFilename))
|
||||||
using (var outputStream = storage.GetStream(Filename, FileAccess.Write, FileMode.Create))
|
using (var outputStream = storage.CreateFileSafely(Filename))
|
||||||
inputStream.CopyTo(outputStream);
|
inputStream.CopyTo(outputStream);
|
||||||
|
|
||||||
storage.Delete(recoveryFilename);
|
storage.Delete(recoveryFilename);
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
@ -118,7 +117,7 @@ namespace osu.Game.Graphics
|
|||||||
|
|
||||||
if (filename == null) return;
|
if (filename == null) return;
|
||||||
|
|
||||||
using (var stream = storage.GetStream(filename, FileAccess.Write))
|
using (var stream = storage.CreateFileSafely(filename))
|
||||||
{
|
{
|
||||||
switch (screenshotFormat.Value)
|
switch (screenshotFormat.Value)
|
||||||
{
|
{
|
||||||
|
@ -5,6 +5,7 @@ using System.Collections.Generic;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
using osu.Game.Graphics.Backgrounds;
|
using osu.Game.Graphics.Backgrounds;
|
||||||
|
|
||||||
namespace osu.Game.Graphics.UserInterface
|
namespace osu.Game.Graphics.UserInterface
|
||||||
@ -27,7 +28,7 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual IEnumerable<string> FilterTerms => new[] { Text.ToString() };
|
public virtual IEnumerable<LocalisableString> FilterTerms => new[] { Text };
|
||||||
|
|
||||||
public bool MatchingFilter
|
public bool MatchingFilter
|
||||||
{
|
{
|
||||||
|
@ -6,6 +6,7 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
|
||||||
namespace osu.Game.Graphics.UserInterfaceV2
|
namespace osu.Game.Graphics.UserInterfaceV2
|
||||||
@ -25,7 +26,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
|||||||
set => Component.ReadOnly = value;
|
set => Component.ReadOnly = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string PlaceholderText
|
public LocalisableString PlaceholderText
|
||||||
{
|
{
|
||||||
set => Component.PlaceholderText = value;
|
set => Component.PlaceholderText = value;
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ using osu.Framework.Graphics.Sprites;
|
|||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
|
||||||
namespace osu.Game.Graphics.UserInterfaceV2
|
namespace osu.Game.Graphics.UserInterfaceV2
|
||||||
{
|
{
|
||||||
@ -44,8 +45,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
|||||||
|
|
||||||
internal class Background : CompositeDrawable
|
internal class Background : CompositeDrawable
|
||||||
{
|
{
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader(true)]
|
||||||
private void load(OsuColour colours)
|
private void load(OverlayColourProvider overlayColourProvider, OsuColour colours)
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
@ -54,7 +55,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
|||||||
|
|
||||||
InternalChild = new Box
|
InternalChild = new Box
|
||||||
{
|
{
|
||||||
Colour = colours.GreySeaFoamDarker,
|
Colour = overlayColourProvider?.Background5 ?? colours.GreySeaFoamDarker,
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ using JetBrains.Annotations;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
|
|
||||||
@ -39,7 +40,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
|||||||
|
|
||||||
private void updateCornerRadius() => Content.CornerRadius = DrawHeight / 2;
|
private void updateCornerRadius() => Content.CornerRadius = DrawHeight / 2;
|
||||||
|
|
||||||
public virtual IEnumerable<string> FilterTerms => new[] { Text.ToString() };
|
public virtual IEnumerable<LocalisableString> FilterTerms => new[] { Text };
|
||||||
|
|
||||||
public bool MatchingFilter
|
public bool MatchingFilter
|
||||||
{
|
{
|
||||||
|
@ -70,6 +70,8 @@ namespace osu.Game.IO
|
|||||||
public override Stream GetStream(string path, FileAccess access = FileAccess.Read, FileMode mode = FileMode.OpenOrCreate) =>
|
public override Stream GetStream(string path, FileAccess access = FileAccess.Read, FileMode mode = FileMode.OpenOrCreate) =>
|
||||||
UnderlyingStorage.GetStream(MutatePath(path), access, mode);
|
UnderlyingStorage.GetStream(MutatePath(path), access, mode);
|
||||||
|
|
||||||
|
public override void Move(string from, string to) => UnderlyingStorage.Move(MutatePath(from), MutatePath(to));
|
||||||
|
|
||||||
public override bool OpenFileExternally(string filename) => UnderlyingStorage.OpenFileExternally(MutatePath(filename));
|
public override bool OpenFileExternally(string filename) => UnderlyingStorage.OpenFileExternally(MutatePath(filename));
|
||||||
|
|
||||||
public override bool PresentFileExternally(string filename) => UnderlyingStorage.PresentFileExternally(MutatePath(filename));
|
public override bool PresentFileExternally(string filename) => UnderlyingStorage.PresentFileExternally(MutatePath(filename));
|
||||||
|
@ -69,6 +69,26 @@ namespace osu.Game.Localisation
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static LocalisableString SelectAll => new TranslatableString(getKey(@"select_all"), @"Select All");
|
public static LocalisableString SelectAll => new TranslatableString(getKey(@"select_all"), @"Select All");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Beatmaps"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString Beatmaps => new TranslatableString(getKey(@"beatmaps"), @"Beatmaps");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Scores"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString Scores => new TranslatableString(getKey(@"scores"), @"Scores");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Skins"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString Skins => new TranslatableString(getKey(@"skins"), @"Skins");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Collections"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString Collections => new TranslatableString(getKey(@"collections"), @"Collections");
|
||||||
|
|
||||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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}";
|
||||||
|
}
|
||||||
|
}
|
@ -1207,6 +1207,8 @@ namespace osu.Game
|
|||||||
Current = newScreen?.GetType().ReadableName(),
|
Current = newScreen?.GetType().ReadableName(),
|
||||||
Previous = current?.GetType().ReadableName(),
|
Previous = current?.GetType().ReadableName(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
scope.SetTag(@"screen", newScreen?.GetType().ReadableName() ?? @"none");
|
||||||
});
|
});
|
||||||
|
|
||||||
switch (newScreen)
|
switch (newScreen)
|
||||||
|
@ -243,7 +243,7 @@ namespace osu.Game
|
|||||||
{
|
{
|
||||||
if (source != null)
|
if (source != null)
|
||||||
{
|
{
|
||||||
using (var destination = Storage.GetStream(Path.Combine(backup_folder, $"collection.{migration}.db"), FileAccess.Write, FileMode.CreateNew))
|
using (var destination = Storage.CreateFileSafely(Path.Combine(backup_folder, $"collection.{migration}.db")))
|
||||||
source.CopyTo(destination);
|
source.CopyTo(destination);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ using osu.Framework.Graphics.Containers;
|
|||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
@ -28,7 +29,7 @@ namespace osu.Game.Overlays.Chat.Listing
|
|||||||
public readonly Channel Channel;
|
public readonly Channel Channel;
|
||||||
|
|
||||||
public bool FilteringActive { get; set; }
|
public bool FilteringActive { get; set; }
|
||||||
public IEnumerable<string> FilterTerms => new[] { Channel.Name, Channel.Topic ?? string.Empty };
|
public IEnumerable<LocalisableString> FilterTerms => new LocalisableString[] { Channel.Name, Channel.Topic ?? string.Empty };
|
||||||
public bool MatchingFilter { set => this.FadeTo(value ? 1f : 0f, 100); }
|
public bool MatchingFilter { set => this.FadeTo(value ? 1f : 0f, 100); }
|
||||||
|
|
||||||
private Box hoverBox = null!;
|
private Box hoverBox = null!;
|
||||||
|
@ -11,6 +11,7 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Online.Chat;
|
using osu.Game.Online.Chat;
|
||||||
@ -36,7 +37,7 @@ namespace osu.Game.Overlays.Chat.Selection
|
|||||||
private Color4 topicColour;
|
private Color4 topicColour;
|
||||||
private Color4 hoverColour;
|
private Color4 hoverColour;
|
||||||
|
|
||||||
public IEnumerable<string> FilterTerms => new[] { Channel.Name, Channel.Topic ?? string.Empty };
|
public IEnumerable<LocalisableString> FilterTerms => new LocalisableString[] { Channel.Name, Channel.Topic ?? string.Empty };
|
||||||
|
|
||||||
public bool MatchingFilter
|
public bool MatchingFilter
|
||||||
{
|
{
|
||||||
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Online.Chat;
|
using osu.Game.Online.Chat;
|
||||||
@ -18,7 +19,7 @@ namespace osu.Game.Overlays.Chat.Selection
|
|||||||
public readonly FillFlowContainer<ChannelListItem> ChannelFlow;
|
public readonly FillFlowContainer<ChannelListItem> ChannelFlow;
|
||||||
|
|
||||||
public IEnumerable<IFilterable> FilterableChildren => ChannelFlow.Children;
|
public IEnumerable<IFilterable> FilterableChildren => ChannelFlow.Children;
|
||||||
public IEnumerable<string> FilterTerms => Array.Empty<string>();
|
public IEnumerable<LocalisableString> FilterTerms => Array.Empty<LocalisableString>();
|
||||||
|
|
||||||
public bool MatchingFilter
|
public bool MatchingFilter
|
||||||
{
|
{
|
||||||
|
@ -39,8 +39,11 @@ namespace osu.Game.Overlays
|
|||||||
private ChatTextBar textBar = null!;
|
private ChatTextBar textBar = null!;
|
||||||
private Container<ChatOverlayDrawableChannel> currentChannelContainer = null!;
|
private Container<ChatOverlayDrawableChannel> currentChannelContainer = null!;
|
||||||
|
|
||||||
private readonly BindableFloat chatHeight = new BindableFloat();
|
private readonly Dictionary<Channel, ChatOverlayDrawableChannel> loadedChannels = new Dictionary<Channel, ChatOverlayDrawableChannel>();
|
||||||
|
|
||||||
|
protected IEnumerable<DrawableChannel> DrawableChannels => loadedChannels.Values;
|
||||||
|
|
||||||
|
private readonly BindableFloat chatHeight = new BindableFloat();
|
||||||
private bool isDraggingTopBar;
|
private bool isDraggingTopBar;
|
||||||
private float dragStartChatHeight;
|
private float dragStartChatHeight;
|
||||||
|
|
||||||
@ -253,38 +256,76 @@ namespace osu.Game.Overlays
|
|||||||
|
|
||||||
if (newChannel is ChannelListing.ChannelListingChannel)
|
if (newChannel is ChannelListing.ChannelListingChannel)
|
||||||
{
|
{
|
||||||
channelListing.State.Value = Visibility.Visible;
|
currentChannelContainer.Clear(false);
|
||||||
|
channelListing.Show();
|
||||||
textBar.ShowSearch.Value = true;
|
textBar.ShowSearch.Value = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
channelListing.State.Value = Visibility.Hidden;
|
channelListing.Hide();
|
||||||
textBar.ShowSearch.Value = false;
|
textBar.ShowSearch.Value = false;
|
||||||
|
|
||||||
loading.Show();
|
if (loadedChannels.ContainsKey(newChannel))
|
||||||
LoadComponentAsync(new ChatOverlayDrawableChannel(newChannel), loaded =>
|
|
||||||
{
|
{
|
||||||
currentChannelContainer.Clear();
|
currentChannelContainer.Clear(false);
|
||||||
currentChannelContainer.Add(loaded);
|
currentChannelContainer.Add(loadedChannels[newChannel]);
|
||||||
loading.Hide();
|
}
|
||||||
});
|
else
|
||||||
|
{
|
||||||
|
loading.Show();
|
||||||
|
|
||||||
|
// Ensure the drawable channel is stored before async load to prevent double loading
|
||||||
|
ChatOverlayDrawableChannel drawableChannel = CreateDrawableChannel(newChannel);
|
||||||
|
loadedChannels.Add(newChannel, drawableChannel);
|
||||||
|
|
||||||
|
LoadComponentAsync(drawableChannel, loadedDrawable =>
|
||||||
|
{
|
||||||
|
// Ensure the current channel hasn't changed by the time the load completes
|
||||||
|
if (currentChannel.Value != loadedDrawable.Channel)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Ensure the cached reference hasn't been removed from leaving the channel
|
||||||
|
if (!loadedChannels.ContainsKey(loadedDrawable.Channel))
|
||||||
|
return;
|
||||||
|
|
||||||
|
currentChannelContainer.Clear(false);
|
||||||
|
currentChannelContainer.Add(loadedDrawable);
|
||||||
|
loading.Hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected virtual ChatOverlayDrawableChannel CreateDrawableChannel(Channel newChannel) => new ChatOverlayDrawableChannel(newChannel);
|
||||||
|
|
||||||
private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args)
|
private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args)
|
||||||
{
|
{
|
||||||
switch (args.Action)
|
switch (args.Action)
|
||||||
{
|
{
|
||||||
case NotifyCollectionChangedAction.Add:
|
case NotifyCollectionChangedAction.Add:
|
||||||
IEnumerable<Channel> newChannels = filterChannels(args.NewItems);
|
IEnumerable<Channel> newChannels = filterChannels(args.NewItems);
|
||||||
|
|
||||||
foreach (var channel in newChannels)
|
foreach (var channel in newChannels)
|
||||||
channelList.AddChannel(channel);
|
channelList.AddChannel(channel);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case NotifyCollectionChangedAction.Remove:
|
case NotifyCollectionChangedAction.Remove:
|
||||||
IEnumerable<Channel> leftChannels = filterChannels(args.OldItems);
|
IEnumerable<Channel> leftChannels = filterChannels(args.OldItems);
|
||||||
|
|
||||||
foreach (var channel in leftChannels)
|
foreach (var channel in leftChannels)
|
||||||
|
{
|
||||||
channelList.RemoveChannel(channel);
|
channelList.RemoveChannel(channel);
|
||||||
|
|
||||||
|
if (loadedChannels.ContainsKey(channel))
|
||||||
|
{
|
||||||
|
ChatOverlayDrawableChannel loaded = loadedChannels[channel];
|
||||||
|
loadedChannels.Remove(channel);
|
||||||
|
// DrawableChannel removed from cache must be manually disposed
|
||||||
|
loaded.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,8 @@ namespace osu.Game.Overlays.FirstRunSetup
|
|||||||
|
|
||||||
protected const float CONTENT_FONT_SIZE = 16;
|
protected const float CONTENT_FONT_SIZE = 16;
|
||||||
|
|
||||||
|
protected const float CONTENT_PADDING = 30;
|
||||||
|
|
||||||
protected const float HEADER_FONT_SIZE = 24;
|
protected const float HEADER_FONT_SIZE = 24;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
@ -41,7 +43,7 @@ namespace osu.Game.Overlays.FirstRunSetup
|
|||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
AutoSizeAxes = Axes.Y,
|
AutoSizeAxes = Axes.Y,
|
||||||
Padding = new MarginPadding { Horizontal = 30 },
|
Padding = new MarginPadding { Horizontal = CONTENT_PADDING },
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
new OsuSpriteText
|
new OsuSpriteText
|
||||||
|
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.Database;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Graphics.UserInterface;
|
|
||||||
using osu.Game.Graphics.UserInterfaceV2;
|
|
||||||
using osu.Game.Localisation;
|
using osu.Game.Localisation;
|
||||||
using osu.Game.Online;
|
using osu.Game.Online;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -27,7 +25,6 @@ namespace osu.Game.Overlays.FirstRunSetup
|
|||||||
public class ScreenBeatmaps : FirstRunSetupScreen
|
public class ScreenBeatmaps : FirstRunSetupScreen
|
||||||
{
|
{
|
||||||
private ProgressRoundedButton downloadBundledButton = null!;
|
private ProgressRoundedButton downloadBundledButton = null!;
|
||||||
private ProgressRoundedButton importBeatmapsButton = null!;
|
|
||||||
private ProgressRoundedButton downloadTutorialButton = null!;
|
private ProgressRoundedButton downloadTutorialButton = null!;
|
||||||
|
|
||||||
private OsuTextFlowContainer currentlyLoadedBeatmaps = null!;
|
private OsuTextFlowContainer currentlyLoadedBeatmaps = null!;
|
||||||
@ -43,8 +40,8 @@ namespace osu.Game.Overlays.FirstRunSetup
|
|||||||
|
|
||||||
private IDisposable? beatmapSubscription;
|
private IDisposable? beatmapSubscription;
|
||||||
|
|
||||||
[BackgroundDependencyLoader(permitNulls: true)]
|
[BackgroundDependencyLoader]
|
||||||
private void load(LegacyImportManager? legacyImportManager)
|
private void load()
|
||||||
{
|
{
|
||||||
Vector2 buttonSize = new Vector2(400, 50);
|
Vector2 buttonSize = new Vector2(400, 50);
|
||||||
|
|
||||||
@ -106,32 +103,6 @@ namespace osu.Game.Overlays.FirstRunSetup
|
|||||||
Action = downloadBundled
|
Action = downloadBundled
|
||||||
},
|
},
|
||||||
new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
|
new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
|
||||||
{
|
|
||||||
Colour = OverlayColourProvider.Content1,
|
|
||||||
Text = "If you have an existing osu! install, you can also choose to import your existing beatmap collection.",
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
AutoSizeAxes = Axes.Y
|
|
||||||
},
|
|
||||||
importBeatmapsButton = new ProgressRoundedButton
|
|
||||||
{
|
|
||||||
Size = buttonSize,
|
|
||||||
Anchor = Anchor.TopCentre,
|
|
||||||
Origin = Anchor.TopCentre,
|
|
||||||
BackgroundColour = colours.Blue3,
|
|
||||||
Text = MaintenanceSettingsStrings.ImportBeatmapsFromStable,
|
|
||||||
Action = () =>
|
|
||||||
{
|
|
||||||
importBeatmapsButton.Enabled.Value = false;
|
|
||||||
legacyImportManager?.ImportFromStableAsync(StableContent.Beatmaps).ContinueWith(t => Schedule(() =>
|
|
||||||
{
|
|
||||||
if (t.IsCompletedSuccessfully)
|
|
||||||
importBeatmapsButton.Complete();
|
|
||||||
else
|
|
||||||
importBeatmapsButton.Enabled.Value = true;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
|
|
||||||
{
|
{
|
||||||
Colour = OverlayColourProvider.Content1,
|
Colour = OverlayColourProvider.Content1,
|
||||||
Text = FirstRunSetupBeatmapScreenStrings.ObtainMoreBeatmaps,
|
Text = FirstRunSetupBeatmapScreenStrings.ObtainMoreBeatmaps,
|
||||||
@ -214,45 +185,5 @@ namespace osu.Game.Overlays.FirstRunSetup
|
|||||||
downloadBundledButton.SetProgress(progress, true);
|
downloadBundledButton.SetProgress(progress, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ProgressRoundedButton : RoundedButton
|
|
||||||
{
|
|
||||||
[Resolved]
|
|
||||||
private OsuColour colours { get; set; } = null!;
|
|
||||||
|
|
||||||
private ProgressBar progressBar = null!;
|
|
||||||
|
|
||||||
protected override void LoadComplete()
|
|
||||||
{
|
|
||||||
base.LoadComplete();
|
|
||||||
|
|
||||||
Add(progressBar = new ProgressBar(false)
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Blending = BlendingParameters.Additive,
|
|
||||||
FillColour = BackgroundColour,
|
|
||||||
Alpha = 0.5f,
|
|
||||||
Depth = float.MinValue
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Complete()
|
|
||||||
{
|
|
||||||
Enabled.Value = false;
|
|
||||||
|
|
||||||
Background.FadeColour(colours.Green, 500, Easing.OutQuint);
|
|
||||||
progressBar.FillColour = colours.Green;
|
|
||||||
|
|
||||||
this.TransformBindableTo(progressBar.Current, 1, 500, Easing.OutQuint);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetProgress(double progress, bool animated)
|
|
||||||
{
|
|
||||||
if (!Enabled.Value)
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.TransformBindableTo(progressBar.Current, progress, animated ? 500 : 0, Easing.OutQuint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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
|
#nullable enable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Cursor;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
@ -17,6 +19,7 @@ using osu.Framework.Localisation;
|
|||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Framework.Threading;
|
using osu.Framework.Threading;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.Database;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Input.Bindings;
|
using osu.Game.Input.Bindings;
|
||||||
@ -55,18 +58,10 @@ namespace osu.Game.Overlays
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public FirstRunSetupScreen? CurrentScreen => (FirstRunSetupScreen?)stack?.CurrentScreen;
|
public FirstRunSetupScreen? CurrentScreen => (FirstRunSetupScreen?)stack?.CurrentScreen;
|
||||||
|
|
||||||
private readonly Type[] steps =
|
private readonly List<Type> steps = new List<Type>();
|
||||||
{
|
|
||||||
typeof(ScreenWelcome),
|
|
||||||
typeof(ScreenBeatmaps),
|
|
||||||
typeof(ScreenUIScale),
|
|
||||||
typeof(ScreenBehaviour),
|
|
||||||
};
|
|
||||||
|
|
||||||
private Container screenContent = null!;
|
private Container screenContent = null!;
|
||||||
|
|
||||||
private Bindable<OverlayActivation>? overlayActivationMode;
|
|
||||||
|
|
||||||
private Container content = null!;
|
private Container content = null!;
|
||||||
|
|
||||||
private LoadingSpinner loading = null!;
|
private LoadingSpinner loading = null!;
|
||||||
@ -77,15 +72,22 @@ namespace osu.Game.Overlays
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader(permitNulls: true)]
|
||||||
private void load(OsuColour colours)
|
private void load(OsuColour colours, LegacyImportManager? legacyImportManager)
|
||||||
{
|
{
|
||||||
|
steps.Add(typeof(ScreenWelcome));
|
||||||
|
steps.Add(typeof(ScreenBeatmaps));
|
||||||
|
if (legacyImportManager?.SupportsImportFromStable == true)
|
||||||
|
steps.Add(typeof(ScreenImportFromStable));
|
||||||
|
steps.Add(typeof(ScreenUIScale));
|
||||||
|
steps.Add(typeof(ScreenBehaviour));
|
||||||
|
|
||||||
Header.Title = FirstRunSetupOverlayStrings.FirstRunSetupTitle;
|
Header.Title = FirstRunSetupOverlayStrings.FirstRunSetupTitle;
|
||||||
Header.Description = FirstRunSetupOverlayStrings.FirstRunSetupDescription;
|
Header.Description = FirstRunSetupOverlayStrings.FirstRunSetupDescription;
|
||||||
|
|
||||||
MainAreaContent.AddRange(new Drawable[]
|
MainAreaContent.AddRange(new Drawable[]
|
||||||
{
|
{
|
||||||
content = new Container
|
content = new PopoverContainer
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
@ -221,16 +223,9 @@ namespace osu.Game.Overlays
|
|||||||
// if we are valid for display, only do so after reaching the main menu.
|
// if we are valid for display, only do so after reaching the main menu.
|
||||||
performer.PerformFromScreen(screen =>
|
performer.PerformFromScreen(screen =>
|
||||||
{
|
{
|
||||||
MainMenu menu = (MainMenu)screen;
|
// Hides the toolbar for us.
|
||||||
|
if (screen is MainMenu menu)
|
||||||
// Eventually I'd like to replace this with a better method that doesn't access the screen.
|
menu.ReturnToOsuLogo();
|
||||||
// Either this dialog would be converted to its own screen, or at very least be "hosted" by a screen pushed to the main menu.
|
|
||||||
// Alternatively, another method of disabling notifications could be added to `INotificationOverlay`.
|
|
||||||
if (menu != null)
|
|
||||||
{
|
|
||||||
overlayActivationMode = menu.OverlayActivationMode.GetBoundCopy();
|
|
||||||
overlayActivationMode.Value = OverlayActivation.UserTriggered;
|
|
||||||
}
|
|
||||||
|
|
||||||
base.Show();
|
base.Show();
|
||||||
}, new[] { typeof(MainMenu) });
|
}, new[] { typeof(MainMenu) });
|
||||||
@ -253,13 +248,6 @@ namespace osu.Game.Overlays
|
|||||||
|
|
||||||
content.ScaleTo(0.99f, 400, Easing.OutQuint);
|
content.ScaleTo(0.99f, 400, Easing.OutQuint);
|
||||||
|
|
||||||
if (overlayActivationMode != null)
|
|
||||||
{
|
|
||||||
// If this is non-null we are guaranteed to have come from the main menu.
|
|
||||||
overlayActivationMode.Value = OverlayActivation.All;
|
|
||||||
overlayActivationMode = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentStepIndex != null)
|
if (currentStepIndex != null)
|
||||||
{
|
{
|
||||||
notificationOverlay.Post(new SimpleNotification
|
notificationOverlay.Post(new SimpleNotification
|
||||||
@ -313,7 +301,7 @@ namespace osu.Game.Overlays
|
|||||||
|
|
||||||
currentStepIndex++;
|
currentStepIndex++;
|
||||||
|
|
||||||
if (currentStepIndex < steps.Length)
|
if (currentStepIndex < steps.Count)
|
||||||
{
|
{
|
||||||
var nextScreen = (Screen)Activator.CreateInstance(steps[currentStepIndex.Value]);
|
var nextScreen = (Screen)Activator.CreateInstance(steps[currentStepIndex.Value]);
|
||||||
|
|
||||||
@ -345,7 +333,7 @@ namespace osu.Game.Overlays
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
bool isFirstStep = currentStepIndex == 0;
|
bool isFirstStep = currentStepIndex == 0;
|
||||||
bool isLastStep = currentStepIndex == steps.Length - 1;
|
bool isLastStep = currentStepIndex == steps.Count - 1;
|
||||||
|
|
||||||
if (isFirstStep)
|
if (isFirstStep)
|
||||||
{
|
{
|
||||||
|
@ -428,9 +428,8 @@ namespace osu.Game.Overlays.Mods
|
|||||||
base.PopIn();
|
base.PopIn();
|
||||||
|
|
||||||
multiplierDisplay?
|
multiplierDisplay?
|
||||||
.Delay(fade_in_duration * 0.65f)
|
.FadeIn(fade_in_duration, Easing.OutQuint)
|
||||||
.FadeIn(fade_in_duration / 2, Easing.OutQuint)
|
.MoveToY(0, fade_in_duration, Easing.OutQuint);
|
||||||
.ScaleTo(1, fade_in_duration, Easing.OutElastic);
|
|
||||||
|
|
||||||
int nonFilteredColumnCount = 0;
|
int nonFilteredColumnCount = 0;
|
||||||
|
|
||||||
@ -465,7 +464,7 @@ namespace osu.Game.Overlays.Mods
|
|||||||
|
|
||||||
multiplierDisplay?
|
multiplierDisplay?
|
||||||
.FadeOut(fade_out_duration / 2, Easing.OutQuint)
|
.FadeOut(fade_out_duration / 2, Easing.OutQuint)
|
||||||
.ScaleTo(0.75f, fade_out_duration, Easing.OutQuint);
|
.MoveToY(-distance, fade_out_duration / 2, Easing.OutQuint);
|
||||||
|
|
||||||
int nonFilteredColumnCount = 0;
|
int nonFilteredColumnCount = 0;
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -113,7 +114,7 @@ namespace osu.Game.Overlays.Music
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<string> FilterTerms => Model.PerformRead(m => m.Metadata.GetSearchableTerms());
|
public IEnumerable<LocalisableString> FilterTerms => Model.PerformRead(m => m.Metadata.GetSearchableTerms()).Select(s => (LocalisableString)s).ToArray();
|
||||||
|
|
||||||
private bool matchingFilter = true;
|
private bool matchingFilter = true;
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
|
|||||||
{
|
{
|
||||||
protected override LocalisableString Header => AudioSettingsStrings.OffsetHeader;
|
protected override LocalisableString Header => AudioSettingsStrings.OffsetHeader;
|
||||||
|
|
||||||
public override IEnumerable<string> FilterTerms => base.FilterTerms.Concat(new[] { "universal", "uo", "timing" });
|
public override IEnumerable<LocalisableString> FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "universal", "uo", "timing" });
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuConfigManager config)
|
private void load(OsuConfigManager config)
|
||||||
|
@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Settings.Sections
|
|||||||
Icon = FontAwesome.Solid.VolumeUp
|
Icon = FontAwesome.Solid.VolumeUp
|
||||||
};
|
};
|
||||||
|
|
||||||
public override IEnumerable<string> FilterTerms => base.FilterTerms.Concat(new[] { "sound" });
|
public override IEnumerable<LocalisableString> FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "sound" });
|
||||||
|
|
||||||
public AudioSection()
|
public AudioSection()
|
||||||
{
|
{
|
||||||
|
@ -14,7 +14,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
|
|||||||
{
|
{
|
||||||
protected override LocalisableString Header => GameplaySettingsStrings.ModsHeader;
|
protected override LocalisableString Header => GameplaySettingsStrings.ModsHeader;
|
||||||
|
|
||||||
public override IEnumerable<string> FilterTerms => base.FilterTerms.Concat(new[] { "mod" });
|
public override IEnumerable<LocalisableString> FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "mod" });
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuConfigManager config)
|
private void load(OsuConfigManager config)
|
||||||
|
@ -15,6 +15,7 @@ using osu.Framework.Graphics.Shapes;
|
|||||||
using osu.Framework.Input;
|
using osu.Framework.Input;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
@ -67,7 +68,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
|||||||
|
|
||||||
private Bindable<bool> isDefault { get; } = new BindableBool(true);
|
private Bindable<bool> isDefault { get; } = new BindableBool(true);
|
||||||
|
|
||||||
public IEnumerable<string> FilterTerms => bindings.Select(b => keyCombinationProvider.GetReadableString(b.KeyCombination)).Prepend(text.Text.ToString());
|
public IEnumerable<LocalisableString> FilterTerms => bindings.Select(b => (LocalisableString)keyCombinationProvider.GetReadableString(b.KeyCombination)).Prepend(text.Text);
|
||||||
|
|
||||||
public KeyBindingRow(object action, List<RealmKeyBinding> bindings)
|
public KeyBindingRow(object action, List<RealmKeyBinding> bindings)
|
||||||
{
|
{
|
||||||
|
@ -6,6 +6,7 @@ using System.Linq;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.Input.Bindings;
|
using osu.Game.Input.Bindings;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
@ -74,6 +75,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Empty FilterTerms so that the ResetButton is visible only when the whole subsection is visible.
|
// Empty FilterTerms so that the ResetButton is visible only when the whole subsection is visible.
|
||||||
public override IEnumerable<string> FilterTerms => Enumerable.Empty<string>();
|
public override IEnumerable<LocalisableString> FilterTerms => Enumerable.Empty<LocalisableString>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,13 +20,12 @@ namespace osu.Game.Overlays.Settings
|
|||||||
|
|
||||||
public LocalisableString TooltipText { get; set; }
|
public LocalisableString TooltipText { get; set; }
|
||||||
|
|
||||||
public override IEnumerable<string> FilterTerms
|
public override IEnumerable<LocalisableString> FilterTerms
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (TooltipText != default)
|
if (TooltipText != default)
|
||||||
// TODO: this won't work as intended once the tooltip text is translated.
|
return base.FilterTerms.Append(TooltipText);
|
||||||
return base.FilterTerms.Append(TooltipText.ToString());
|
|
||||||
|
|
||||||
return base.FilterTerms;
|
return base.FilterTerms;
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
|
||||||
namespace osu.Game.Overlays.Settings
|
namespace osu.Game.Overlays.Settings
|
||||||
@ -25,7 +26,7 @@ namespace osu.Game.Overlays.Settings
|
|||||||
set => Control.ItemSource = value;
|
set => Control.ItemSource = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IEnumerable<string> FilterTerms => base.FilterTerms.Concat(Control.Items.Select(i => i.ToString()));
|
public override IEnumerable<LocalisableString> FilterTerms => base.FilterTerms.Concat(Control.Items.Select(i => (LocalisableString)i.ToString()));
|
||||||
|
|
||||||
protected sealed override Drawable CreateControl() => CreateDropdown();
|
protected sealed override Drawable CreateControl() => CreateDropdown();
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -98,13 +99,13 @@ namespace osu.Game.Overlays.Settings
|
|||||||
set => controlWithCurrent.Current = value;
|
set => controlWithCurrent.Current = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual IEnumerable<string> FilterTerms
|
public virtual IEnumerable<LocalisableString> FilterTerms
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
var keywords = new List<string>(Keywords ?? Array.Empty<string>())
|
var keywords = new List<LocalisableString>(Keywords?.Select(k => (LocalisableString)k) ?? Array.Empty<LocalisableString>())
|
||||||
{
|
{
|
||||||
LabelText.ToString()
|
LabelText
|
||||||
};
|
};
|
||||||
|
|
||||||
if (HasClassicDefault)
|
if (HasClassicDefault)
|
||||||
|
@ -32,7 +32,7 @@ namespace osu.Game.Overlays.Settings
|
|||||||
public abstract LocalisableString Header { get; }
|
public abstract LocalisableString Header { get; }
|
||||||
|
|
||||||
public IEnumerable<IFilterable> FilterableChildren => Children.OfType<IFilterable>();
|
public IEnumerable<IFilterable> FilterableChildren => Children.OfType<IFilterable>();
|
||||||
public virtual IEnumerable<string> FilterTerms => new[] { Header.ToString() };
|
public virtual IEnumerable<LocalisableString> FilterTerms => new[] { Header };
|
||||||
|
|
||||||
public const int ITEM_SPACING = 14;
|
public const int ITEM_SPACING = 14;
|
||||||
|
|
||||||
|
@ -25,11 +25,7 @@ namespace osu.Game.Overlays.Settings
|
|||||||
|
|
||||||
public IEnumerable<IFilterable> FilterableChildren => Children.OfType<IFilterable>();
|
public IEnumerable<IFilterable> FilterableChildren => Children.OfType<IFilterable>();
|
||||||
|
|
||||||
// FilterTerms should contains both original string and localised string for user to search.
|
public virtual IEnumerable<LocalisableString> FilterTerms => new[] { Header };
|
||||||
// Since LocalisableString is unable to get original string at this time (2021-08-14),
|
|
||||||
// only call .ToString() to use localised one.
|
|
||||||
// TODO: Update here when FilterTerms accept LocalisableString.
|
|
||||||
public virtual IEnumerable<string> FilterTerms => new[] { Header.ToString() };
|
|
||||||
|
|
||||||
public bool MatchingFilter
|
public bool MatchingFilter
|
||||||
{
|
{
|
||||||
|
@ -7,6 +7,7 @@ using System.Collections.Specialized;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.EnumExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Input;
|
using osu.Framework.Input;
|
||||||
@ -361,20 +362,23 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
/// <returns>The most relevant <see cref="Playfield"/>.</returns>
|
/// <returns>The most relevant <see cref="Playfield"/>.</returns>
|
||||||
protected virtual Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => drawableRulesetWrapper.Playfield;
|
protected virtual Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => drawableRulesetWrapper.Playfield;
|
||||||
|
|
||||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
|
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||||
{
|
{
|
||||||
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
|
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
|
||||||
double? targetTime = null;
|
double? targetTime = null;
|
||||||
|
|
||||||
if (playfield is ScrollingPlayfield scrollingPlayfield)
|
if (snapType.HasFlagFast(SnapType.Grids))
|
||||||
{
|
{
|
||||||
targetTime = scrollingPlayfield.TimeAtScreenSpacePosition(screenSpacePosition);
|
if (playfield is ScrollingPlayfield scrollingPlayfield)
|
||||||
|
{
|
||||||
|
targetTime = scrollingPlayfield.TimeAtScreenSpacePosition(screenSpacePosition);
|
||||||
|
|
||||||
// apply beat snapping
|
// apply beat snapping
|
||||||
targetTime = BeatSnapProvider.SnapTime(targetTime.Value);
|
targetTime = BeatSnapProvider.SnapTime(targetTime.Value);
|
||||||
|
|
||||||
// convert back to screen space
|
// convert back to screen space
|
||||||
screenSpacePosition = scrollingPlayfield.ScreenSpacePositionAtTime(targetTime.Value);
|
screenSpacePosition = scrollingPlayfield.ScreenSpacePositionAtTime(targetTime.Value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new SnapResult(screenSpacePosition, targetTime, playfield);
|
return new SnapResult(screenSpacePosition, targetTime, playfield);
|
||||||
@ -414,10 +418,7 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
|
|
||||||
#region IPositionSnapProvider
|
#region IPositionSnapProvider
|
||||||
|
|
||||||
public abstract SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition);
|
public abstract SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All);
|
||||||
|
|
||||||
public virtual SnapResult FindSnappedPosition(Vector2 screenSpacePosition) =>
|
|
||||||
new SnapResult(screenSpacePosition, null);
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,6 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A snap provider which given a proposed position for a hit object, potentially offers a more correct position and time value inferred from the context of the beatmap.
|
/// A snap provider which given a proposed position for a hit object, potentially offers a more correct position and time value inferred from the context of the beatmap.
|
||||||
/// Provided values are inferred in an isolated context, without consideration of other nearby hit objects.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Cached]
|
[Cached]
|
||||||
public interface IPositionSnapProvider
|
public interface IPositionSnapProvider
|
||||||
@ -16,18 +15,9 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Given a position, find a valid time and position snap.
|
/// Given a position, find a valid time and position snap.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
|
||||||
/// This call should be equivalent to running <see cref="FindSnappedPosition"/> with any additional logic that can be performed without the time immutability restriction.
|
|
||||||
/// </remarks>
|
|
||||||
/// <param name="screenSpacePosition">The screen-space position to be snapped.</param>
|
/// <param name="screenSpacePosition">The screen-space position to be snapped.</param>
|
||||||
|
/// <param name="snapType">The type of snapping to apply.</param>
|
||||||
/// <returns>The time and position post-snapping.</returns>
|
/// <returns>The time and position post-snapping.</returns>
|
||||||
SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition);
|
SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Given a position, find a valid position snap, without changing the time value.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="screenSpacePosition">The screen-space position to be snapped.</param>
|
|
||||||
/// <returns>The position post-snapping. Time will always be null.</returns>
|
|
||||||
SnapResult FindSnappedPosition(Vector2 screenSpacePosition);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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)
|
while (++endIndex < vertices.Length - endPointLength)
|
||||||
{
|
{
|
||||||
// Keep incrementing while an implicit segment doesn't need to be started
|
// Keep incrementing while an implicit segment doesn't need to be started.
|
||||||
if (vertices[endIndex].Position != vertices[endIndex - 1].Position)
|
if (vertices[endIndex].Position != vertices[endIndex - 1].Position)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
// Adjacent legacy Catmull segments should be treated as a single segment.
|
||||||
|
if (FormatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION && type == PathType.Catmull)
|
||||||
|
continue;
|
||||||
|
|
||||||
// The last control point of each segment is not allowed to start a new implicit segment.
|
// The last control point of each segment is not allowed to start a new implicit segment.
|
||||||
if (endIndex == vertices.Length - endPointLength - 1)
|
if (endIndex == vertices.Length - endPointLength - 1)
|
||||||
continue;
|
continue;
|
||||||
|
@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
|||||||
public new ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)base.HitObjectContainer;
|
public new ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)base.HitObjectContainer;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
protected IScrollingInfo ScrollingInfo { get; private set; }
|
public IScrollingInfo ScrollingInfo { get; private set; }
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
|
@ -486,7 +486,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
Vector2 originalPosition = movementBlueprintOriginalPositions[i];
|
Vector2 originalPosition = movementBlueprintOriginalPositions[i];
|
||||||
var testPosition = originalPosition + distanceTravelled;
|
var testPosition = originalPosition + distanceTravelled;
|
||||||
|
|
||||||
var positionalResult = snapProvider.FindSnappedPosition(testPosition);
|
var positionalResult = snapProvider.FindSnappedPositionAndTime(testPosition, SnapType.NearbyObjects);
|
||||||
|
|
||||||
if (positionalResult.ScreenSpacePosition == testPosition) continue;
|
if (positionalResult.ScreenSpacePosition == testPosition) continue;
|
||||||
|
|
||||||
@ -505,7 +505,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
Vector2 movePosition = movementBlueprintOriginalPositions.First() + distanceTravelled;
|
Vector2 movePosition = movementBlueprintOriginalPositions.First() + distanceTravelled;
|
||||||
|
|
||||||
// Retrieve a snapped position.
|
// Retrieve a snapped position.
|
||||||
var result = snapProvider?.FindSnappedPositionAndTime(movePosition);
|
var result = snapProvider?.FindSnappedPositionAndTime(movePosition, ~SnapType.NearbyObjects);
|
||||||
|
|
||||||
if (result == null)
|
if (result == null)
|
||||||
{
|
{
|
||||||
|
@ -154,7 +154,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
private void updateBankPlaceholderText(IEnumerable<HitObject> objects)
|
private void updateBankPlaceholderText(IEnumerable<HitObject> objects)
|
||||||
{
|
{
|
||||||
string? commonBank = getCommonBank(objects.Select(h => h.SampleControlPoint).ToArray());
|
string? commonBank = getCommonBank(objects.Select(h => h.SampleControlPoint).ToArray());
|
||||||
bank.PlaceholderText = string.IsNullOrEmpty(commonBank) ? "(multiple)" : null;
|
bank.PlaceholderText = string.IsNullOrEmpty(commonBank) ? "(multiple)" : string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateVolumeFor(IEnumerable<HitObject> objects, int? newVolume)
|
private void updateVolumeFor(IEnumerable<HitObject> objects, int? newVolume)
|
||||||
|
@ -303,10 +303,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public double VisibleRange => track.Length / Zoom;
|
public double VisibleRange => track.Length / Zoom;
|
||||||
|
|
||||||
public SnapResult FindSnappedPosition(Vector2 screenSpacePosition) =>
|
public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) =>
|
||||||
new SnapResult(screenSpacePosition, null);
|
|
||||||
|
|
||||||
public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) =>
|
|
||||||
new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition))));
|
new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition))));
|
||||||
|
|
||||||
private double getTimeFromPosition(Vector2 localPosition) =>
|
private double getTimeFromPosition(Vector2 localPosition) =>
|
||||||
|
@ -11,11 +11,8 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Cursor;
|
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Framework.Input.Events;
|
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.Graphics.UserInterface;
|
|
||||||
using osu.Game.Graphics.UserInterfaceV2;
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
@ -24,7 +21,7 @@ namespace osu.Game.Screens.Edit.Setup
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A labelled textbox which reveals an inline file chooser when clicked.
|
/// A labelled textbox which reveals an inline file chooser when clicked.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class FileChooserLabelledTextBox : LabelledTextBox, ICanAcceptFiles, IHasPopover
|
internal class FileChooserLabelledTextBox : LabelledTextBoxWithPopover, ICanAcceptFiles
|
||||||
{
|
{
|
||||||
private readonly string[] handledExtensions;
|
private readonly string[] handledExtensions;
|
||||||
|
|
||||||
@ -40,16 +37,6 @@ namespace osu.Game.Screens.Edit.Setup
|
|||||||
this.handledExtensions = handledExtensions;
|
this.handledExtensions = handledExtensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override OsuTextBox CreateTextBox() =>
|
|
||||||
new FileChooserOsuTextBox
|
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
CornerRadius = CORNER_RADIUS,
|
|
||||||
OnFocused = this.ShowPopover
|
|
||||||
};
|
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
@ -81,27 +68,7 @@ namespace osu.Game.Screens.Edit.Setup
|
|||||||
game.UnregisterImportHandler(this);
|
game.UnregisterImportHandler(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class FileChooserOsuTextBox : OsuTextBox
|
public override Popover GetPopover() => new FileChooserPopover(handledExtensions, currentFile);
|
||||||
{
|
|
||||||
public Action OnFocused;
|
|
||||||
|
|
||||||
protected override bool OnDragStart(DragStartEvent e)
|
|
||||||
{
|
|
||||||
// This text box is intended to be "read only" without actually specifying that.
|
|
||||||
// As such we don't want to allow the user to select its content with a drag.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnFocus(FocusEvent e)
|
|
||||||
{
|
|
||||||
OnFocused?.Invoke();
|
|
||||||
base.OnFocus(e);
|
|
||||||
|
|
||||||
GetContainingInputManager().TriggerFocusContention(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Popover GetPopover() => new FileChooserPopover(handledExtensions, currentFile);
|
|
||||||
|
|
||||||
private class FileChooserPopover : OsuPopover
|
private class FileChooserPopover : OsuPopover
|
||||||
{
|
{
|
||||||
|
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)]
|
[Resolved(canBeNull: true)]
|
||||||
private IPerformFromScreenRunner performer { get; set; }
|
private IPerformFromScreenRunner performer { get; set; }
|
||||||
|
|
||||||
|
public void ReturnToOsuLogo() => Buttons.State = ButtonSystemState.Initial;
|
||||||
|
|
||||||
private void confirmAndExit()
|
private void confirmAndExit()
|
||||||
{
|
{
|
||||||
if (exitConfirmed) return;
|
if (exitConfirmed) return;
|
||||||
|
@ -80,7 +80,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
|
|||||||
matchingFilter &= criteria.Ruleset == null || r.Room.PlaylistItemStats.Value?.RulesetIDs.Any(id => id == criteria.Ruleset.OnlineID) != false;
|
matchingFilter &= criteria.Ruleset == null || r.Room.PlaylistItemStats.Value?.RulesetIDs.Any(id => id == criteria.Ruleset.OnlineID) != false;
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(criteria.SearchString))
|
if (!string.IsNullOrEmpty(criteria.SearchString))
|
||||||
matchingFilter &= r.FilterTerms.Any(term => term.Contains(criteria.SearchString, StringComparison.InvariantCultureIgnoreCase));
|
{
|
||||||
|
// Room name isn't translatable, so ToString() is used here for simplicity.
|
||||||
|
matchingFilter &= r.FilterTerms.Any(term => term.ToString().Contains(criteria.SearchString, StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
r.MatchingFilter = matchingFilter;
|
r.MatchingFilter = matchingFilter;
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ using osu.Framework.Graphics.Shapes;
|
|||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
using osu.Game.Extensions;
|
using osu.Game.Extensions;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
@ -101,7 +102,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
|
|||||||
|
|
||||||
public bool FilteringActive { get; set; }
|
public bool FilteringActive { get; set; }
|
||||||
|
|
||||||
public IEnumerable<string> FilterTerms => new[] { Room.Name.Value };
|
public IEnumerable<LocalisableString> FilterTerms => new LocalisableString[] { Room.Name.Value };
|
||||||
|
|
||||||
private bool matchingFilter = true;
|
private bool matchingFilter = true;
|
||||||
|
|
||||||
|
@ -12,7 +12,6 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Utils;
|
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.Drawables;
|
using osu.Game.Beatmaps.Drawables;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
@ -416,13 +415,13 @@ namespace osu.Game.Screens.Select
|
|||||||
foreach (var mod in mods.Value.OfType<IApplicableToRate>())
|
foreach (var mod in mods.Value.OfType<IApplicableToRate>())
|
||||||
rate = mod.ApplyToRate(0, rate);
|
rate = mod.ApplyToRate(0, rate);
|
||||||
|
|
||||||
double bpmMax = beatmap.ControlPointInfo.BPMMaximum * rate;
|
int bpmMax = (int)Math.Round(Math.Round(beatmap.ControlPointInfo.BPMMaximum) * rate);
|
||||||
double bpmMin = beatmap.ControlPointInfo.BPMMinimum * rate;
|
int bpmMin = (int)Math.Round(Math.Round(beatmap.ControlPointInfo.BPMMinimum) * rate);
|
||||||
double mostCommonBPM = 60000 / beatmap.GetMostCommonBeatLength() * rate;
|
int mostCommonBPM = (int)Math.Round(Math.Round(60000 / beatmap.GetMostCommonBeatLength()) * rate);
|
||||||
|
|
||||||
string labelText = Precision.AlmostEquals(bpmMin, bpmMax)
|
string labelText = bpmMin == bpmMax
|
||||||
? $"{bpmMin:0}"
|
? $"{bpmMin}"
|
||||||
: $"{bpmMin:0}-{bpmMax:0} (mostly {mostCommonBPM:0})";
|
: $"{bpmMin}-{bpmMax} (mostly {mostCommonBPM})";
|
||||||
|
|
||||||
bpmLabelContainer.Child = new InfoLabel(new BeatmapStatistic
|
bpmLabelContainer.Child = new InfoLabel(new BeatmapStatistic
|
||||||
{
|
{
|
||||||
|
@ -65,7 +65,7 @@ namespace osu.Game.Stores
|
|||||||
{
|
{
|
||||||
data.Seek(0, SeekOrigin.Begin);
|
data.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
using (var output = Storage.GetStream(file.GetStoragePath(), FileAccess.Write))
|
using (var output = Storage.CreateFileSafely(file.GetStoragePath()))
|
||||||
data.CopyTo(output);
|
data.CopyTo(output);
|
||||||
|
|
||||||
data.Seek(0, SeekOrigin.Begin);
|
data.Seek(0, SeekOrigin.Begin);
|
||||||
|
@ -8,6 +8,7 @@ using System.Diagnostics;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using osu.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
@ -18,6 +19,7 @@ using osu.Game.Database;
|
|||||||
using osu.Game.Models;
|
using osu.Game.Models;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using Sentry;
|
using Sentry;
|
||||||
using Sentry.Protocol;
|
using Sentry.Protocol;
|
||||||
@ -109,6 +111,7 @@ namespace osu.Game.Utils
|
|||||||
}, scope =>
|
}, scope =>
|
||||||
{
|
{
|
||||||
var beatmap = game.Dependencies.Get<IBindable<WorkingBeatmap>>().Value.BeatmapInfo;
|
var beatmap = game.Dependencies.Get<IBindable<WorkingBeatmap>>().Value.BeatmapInfo;
|
||||||
|
var ruleset = game.Dependencies.Get<IBindable<RulesetInfo>>().Value;
|
||||||
|
|
||||||
scope.Contexts[@"config"] = new
|
scope.Contexts[@"config"] = new
|
||||||
{
|
{
|
||||||
@ -125,6 +128,8 @@ namespace osu.Game.Utils
|
|||||||
BeatmapSets = realm.All<BeatmapSetInfo>().Count(),
|
BeatmapSets = realm.All<BeatmapSetInfo>().Count(),
|
||||||
Beatmaps = realm.All<BeatmapInfo>().Count(),
|
Beatmaps = realm.All<BeatmapInfo>().Count(),
|
||||||
Files = realm.All<RealmFile>().Count(),
|
Files = realm.All<RealmFile>().Count(),
|
||||||
|
Rulesets = realm.All<RulesetInfo>().Count(),
|
||||||
|
RulesetsAvailable = realm.All<RulesetInfo>().Count(r => r.Available),
|
||||||
Skins = realm.All<SkinInfo>().Count(),
|
Skins = realm.All<SkinInfo>().Count(),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -137,14 +142,27 @@ namespace osu.Game.Utils
|
|||||||
scope.Contexts[@"beatmap"] = new
|
scope.Contexts[@"beatmap"] = new
|
||||||
{
|
{
|
||||||
Name = beatmap.ToString(),
|
Name = beatmap.ToString(),
|
||||||
|
Ruleset = beatmap.Ruleset.InstantiationInfo,
|
||||||
beatmap.OnlineID,
|
beatmap.OnlineID,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
scope.Contexts[@"ruleset"] = new
|
||||||
|
{
|
||||||
|
ruleset.ShortName,
|
||||||
|
ruleset.Name,
|
||||||
|
ruleset.InstantiationInfo,
|
||||||
|
ruleset.OnlineID
|
||||||
|
};
|
||||||
|
|
||||||
scope.Contexts[@"clocks"] = new
|
scope.Contexts[@"clocks"] = new
|
||||||
{
|
{
|
||||||
Audio = game.Dependencies.Get<MusicController>().CurrentTrack.CurrentTime,
|
Audio = game.Dependencies.Get<MusicController>().CurrentTrack.CurrentTime,
|
||||||
Game = game.Clock.CurrentTime,
|
Game = game.Clock.CurrentTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
scope.SetTag(@"ruleset", ruleset.ShortName);
|
||||||
|
scope.SetTag(@"os", $"{RuntimeInfo.OS} ({Environment.OSVersion})");
|
||||||
|
scope.SetTag(@"processor count", Environment.ProcessorCount.ToString());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user