1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-13 08:32:57 +08:00

Merge branch 'master' into fix-combo-handling

This commit is contained in:
Dan Balasescu 2023-11-23 16:22:01 +09:00
commit 7bc2d6e6cd
No known key found for this signature in database
113 changed files with 2169 additions and 547 deletions

View File

@ -21,10 +21,10 @@
]
},
"ppy.localisationanalyser.tools": {
"version": "2023.712.0",
"version": "2023.1117.0",
"commands": [
"localisation"
]
}
}
}
}

View File

@ -108,6 +108,12 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Setup JDK 11
uses: actions/setup-java@v3
with:
distribution: microsoft
java-version: 11
- name: Install .NET 6.0.x
uses: actions/setup-dotnet@v3
with:
@ -121,24 +127,14 @@ jobs:
build-only-ios:
name: Build only (iOS)
# `macos-13` is required, because Xcode 14.3 is required (see below).
# TODO: can be changed to `macos-latest` once `macos-13` becomes latest (currently in beta)
# `macos-13` is required, because the newest Microsoft.iOS.Sdk versions require Xcode 14.3.
# TODO: can be changed to `macos-latest` once `macos-13` becomes latest (currently in beta: https://github.com/actions/runner-images/tree/main#available-images)
runs-on: macos-13
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v3
# newest Microsoft.iOS.Sdk versions require Xcode 14.3.
# 14.3 is currently not the default Xcode version (https://github.com/actions/runner-images/blob/main/images/macos/macos-13-Readme.md#xcode),
# so set it manually.
# TODO: remove when 14.3 becomes the default Xcode version.
- name: Set Xcode version
shell: bash
run: |
sudo xcode-select -s "/Applications/Xcode_14.3.app"
echo "MD_APPLE_SDK_ROOT=/Applications/Xcode_14.3.app" >> $GITHUB_ENV
- name: Install .NET 6.0.x
uses: actions/setup-dotnet@v3
with:

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1111.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1121.1" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.

View File

@ -28,9 +28,9 @@ namespace osu.Game.Rulesets.Catch.Tests
private class TestLegacySkin : LegacySkin
{
public TestLegacySkin(SkinInfo skin, IResourceStore<byte[]> storage)
public TestLegacySkin(SkinInfo skin, IResourceStore<byte[]> fallbackStore)
// Bypass LegacySkinResourceStore to avoid returning null for retrieving files due to bad skin info (SkinInfo.Files = null).
: base(skin, null, storage)
: base(skin, null, fallbackStore)
{
}
}

View File

@ -140,7 +140,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddStep("update hit object path", () =>
{
hitObject.Path = new SliderPath(PathType.PerfectCurve, new[]
hitObject.Path = new SliderPath(PathType.PERFECT_CURVE, new[]
{
Vector2.Zero,
new Vector2(100, 100),
@ -190,16 +190,16 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
[Test]
public void TestVertexResampling()
{
addBlueprintStep(100, 100, new SliderPath(PathType.PerfectCurve, new[]
addBlueprintStep(100, 100, new SliderPath(PathType.PERFECT_CURVE, new[]
{
Vector2.Zero,
new Vector2(100, 100),
new Vector2(50, 200),
}), 0.5);
AddAssert("1 vertex per 1 nested HO", () => getVertices().Count == hitObject.NestedHitObjects.Count);
AddAssert("slider path not yet changed", () => hitObject.Path.ControlPoints[0].Type == PathType.PerfectCurve);
AddAssert("slider path not yet changed", () => hitObject.Path.ControlPoints[0].Type == PathType.PERFECT_CURVE);
addAddVertexSteps(150, 150);
AddAssert("slider path change to linear", () => hitObject.Path.ControlPoints[0].Type == PathType.Linear);
AddAssert("slider path change to linear", () => hitObject.Path.ControlPoints[0].Type == PathType.LINEAR);
}
private void addBlueprintStep(double time, float x, SliderPath sliderPath, double velocity) => AddStep("add selection blueprint", () =>

View File

@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Catch.Tests
} while (rng.Next(2) != 0);
int length = sliderPath.ControlPoints.Count - start + 1;
sliderPath.ControlPoints[start].Type = length <= 2 ? PathType.Linear : length == 3 ? PathType.PerfectCurve : PathType.Bezier;
sliderPath.ControlPoints[start].Type = length <= 2 ? PathType.LINEAR : length == 3 ? PathType.PERFECT_CURVE : PathType.BEZIER;
} while (rng.Next(3) != 0);
if (rng.Next(5) == 0)
@ -215,7 +215,7 @@ namespace osu.Game.Rulesets.Catch.Tests
foreach (var point in sliderPath.ControlPoints)
{
Assert.That(point.Type, Is.EqualTo(PathType.Linear).Or.Null);
Assert.That(point.Type, Is.EqualTo(PathType.LINEAR).Or.Null);
Assert.That(sliderStartY + point.Position.Y, Is.InRange(0, JuiceStreamPath.OSU_PLAYFIELD_HEIGHT));
}

View File

@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods
var stream = new JuiceStream
{
StartTime = 1000,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(100, 0),

View File

@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods
{
X = CatchPlayfield.CENTER_X,
StartTime = 3000,
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 })
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, Vector2.UnitY * 200 })
}
}
}

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Tests
beatmap.HitObjects.Add(new JuiceStream
{
X = CatchPlayfield.CENTER_X - width / 2,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(width, 0)

View File

@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.Tests
new JuiceStream
{
StartTime = 1000,
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(0, -192) }),
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(0, -192) }),
X = CatchPlayfield.WIDTH / 2
}
}

View File

@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{
X = xCoords,
StartTime = playfieldTime + 1000,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(0, 200)

View File

@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Catch.Tests
new JuiceStream
{
X = CatchPlayfield.CENTER_X,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(0, 100)

View File

@ -74,7 +74,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
path.ConvertFromSliderPath(sliderPath, hitObject.Velocity);
// If the original slider path has non-linear type segments, resample the vertices at nested hit object times to reduce the number of vertices.
if (sliderPath.ControlPoints.Any(p => p.Type != null && p.Type != PathType.Linear))
if (sliderPath.ControlPoints.Any(p => p.Type != null && p.Type != PathType.LINEAR))
{
path.ResampleVertices(hitObject.NestedHitObjects
.Skip(1).TakeWhile(h => !(h is Fruit)) // Only droplets in the first span are used.

View File

@ -236,7 +236,7 @@ namespace osu.Game.Rulesets.Catch.Objects
for (int i = 1; i < vertices.Count; i++)
{
sliderPath.ControlPoints[^1].Type = PathType.Linear;
sliderPath.ControlPoints[^1].Type = PathType.LINEAR;
float deltaX = vertices[i].X - lastPosition.X;
double length = (vertices[i].Time - currentTime) * velocity;

View File

@ -0,0 +1,94 @@
// 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.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public partial class TestSceneOpenEditorTimestampInMania : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new ManiaRuleset();
[Test]
public void TestNormalSelection()
{
addStepClickLink("00:05:920 (5920|3,6623|3,6857|2,7326|1)");
AddAssert("selected group", () => checkSnapAndSelectColumn(5_920, new List<(int, int)>
{ (5_920, 3), (6_623, 3), (6_857, 2), (7_326, 1) }
));
addReset();
addStepClickLink("00:42:716 (42716|3,43420|2,44123|0,44357|1,45295|1)");
AddAssert("selected ungrouped", () => checkSnapAndSelectColumn(42_716, new List<(int, int)>
{ (42_716, 3), (43_420, 2), (44_123, 0), (44_357, 1), (45_295, 1) }
));
addReset();
AddStep("add notes to row", () =>
{
if (EditorBeatmap.HitObjects.Any(x => x is ManiaHitObject m && m.StartTime == 11_545 && m.Column is 1 or 2 or 3))
return;
ManiaHitObject first = (ManiaHitObject)EditorBeatmap.HitObjects.First(x => x is ManiaHitObject m && m.StartTime == 11_545 && m.Column == 0);
ManiaHitObject second = new Note { Column = 1, StartTime = first.StartTime };
ManiaHitObject third = new Note { Column = 2, StartTime = first.StartTime };
ManiaHitObject forth = new Note { Column = 3, StartTime = first.StartTime };
EditorBeatmap.AddRange(new[] { second, third, forth });
});
addStepClickLink("00:11:545 (11545|0,11545|1,11545|2,11545|3)");
AddAssert("selected in row", () => checkSnapAndSelectColumn(11_545, new List<(int, int)>
{ (11_545, 0), (11_545, 1), (11_545, 2), (11_545, 3) }
));
addReset();
addStepClickLink("01:36:623 (96623|1,97560|1,97677|1,97795|1,98966|1)");
AddAssert("selected in column", () => checkSnapAndSelectColumn(96_623, new List<(int, int)>
{ (96_623, 1), (97_560, 1), (97_677, 1), (97_795, 1), (98_966, 1) }
));
}
[Test]
public void TestUnusualSelection()
{
addStepClickLink("00:00:000 (0|1)", "wrong offset");
AddAssert("snap to 1, select none", () => checkSnapAndSelectColumn(2_170));
addReset();
addStepClickLink("00:00:000 (2)", "std link");
AddAssert("snap to 1, select none", () => checkSnapAndSelectColumn(2_170));
addReset();
addStepClickLink("00:00:000 (1,2)", "std link");
AddAssert("snap to 1, select none", () => checkSnapAndSelectColumn(2_170));
}
private void addStepClickLink(string timestamp, string step = "", bool displayTimestamp = true)
{
AddStep(displayTimestamp ? $"{step} {timestamp}" : step, () => Editor.HandleTimestamp(timestamp));
AddUntilStep("wait for seek", () => EditorClock.SeekingOrStopped.Value);
}
private void addReset() => addStepClickLink("00:00:000", "reset", false);
private bool checkSnapAndSelectColumn(double startTime, IReadOnlyCollection<(int, int)>? columnPairs = null)
{
bool checkColumns = columnPairs != null
? EditorBeatmap.SelectedHitObjects.All(x => columnPairs.Any(col => isNoteAt(x, col.Item1, col.Item2)))
: !EditorBeatmap.SelectedHitObjects.Any();
return EditorClock.CurrentTime == startTime
&& EditorBeatmap.SelectedHitObjects.Count == (columnPairs?.Count ?? 0)
&& checkColumns;
}
private bool isNoteAt(HitObject hitObject, double time, int column) =>
hitObject is ManiaHitObject maniaHitObject
&& maniaHitObject.StartTime == time
&& maniaHitObject.Column == column;
}
}

View File

@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
@ -50,5 +51,37 @@ namespace osu.Game.Rulesets.Mania.Edit
public override string ConvertSelectionToString()
=> string.Join(',', EditorBeatmap.SelectedHitObjects.Cast<ManiaHitObject>().OrderBy(h => h.StartTime).Select(h => $"{h.StartTime}|{h.Column}"));
// 123|0,456|1,789|2 ...
private static readonly Regex selection_regex = new Regex(@"^\d+\|\d+(,\d+\|\d+)*$", RegexOptions.Compiled);
public override void SelectFromTimestamp(double timestamp, string objectDescription)
{
if (!selection_regex.IsMatch(objectDescription))
return;
List<ManiaHitObject> remainingHitObjects = EditorBeatmap.HitObjects.Cast<ManiaHitObject>().Where(h => h.StartTime >= timestamp).ToList();
string[] objectDescriptions = objectDescription.Split(',').ToArray();
for (int i = 0; i < objectDescriptions.Length; i++)
{
string[] split = objectDescriptions[i].Split('|').ToArray();
if (split.Length != 2)
continue;
if (!double.TryParse(split[0], out double time) || !int.TryParse(split[1], out int column))
continue;
ManiaHitObject current = remainingHitObjects.FirstOrDefault(h => h.StartTime == time && h.Column == column);
if (current == null)
continue;
EditorBeatmap.SelectedHitObjects.Add(current);
if (i < objectDescriptions.Length - 1)
remainingHitObjects = remainingHitObjects.Where(h => h != current && h.StartTime >= current.StartTime).ToList();
}
}
}
}

View File

@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
Position = new Vector2(420, 240),
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
new PathControlPoint(new Vector2(0, 0), PathType.LINEAR),
new PathControlPoint(new Vector2(-100, 0))
}),
}
@ -128,7 +128,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
Position = playfield_centre,
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
new PathControlPoint(new Vector2(0, 0), PathType.LINEAR),
new PathControlPoint(new Vector2(0, -playfield_centre.Y + 5))
}),
}
@ -149,7 +149,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
Position = playfield_centre,
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
new PathControlPoint(new Vector2(0, 0), PathType.LINEAR),
new PathControlPoint(new Vector2(0, -playfield_centre.Y + 5))
}),
StackHeight = 5
@ -171,7 +171,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
Position = new Vector2(0, 0),
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
new PathControlPoint(new Vector2(0, 0), PathType.LINEAR),
new PathControlPoint(playfield_centre)
}),
}
@ -192,7 +192,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
Position = playfield_centre,
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
new PathControlPoint(new Vector2(0, 0), PathType.LINEAR),
new PathControlPoint(-playfield_centre)
}),
}
@ -214,7 +214,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
Path = new SliderPath(new[]
{
// Circular arc shoots over the top of the screen.
new PathControlPoint(new Vector2(0, 0), PathType.PerfectCurve),
new PathControlPoint(new Vector2(0, 0), PathType.PERFECT_CURVE),
new PathControlPoint(new Vector2(-100, -200)),
new PathControlPoint(new Vector2(100, -200))
}),

View File

@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
mergeSelection();
AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor(
(pos: circle1.Position, pathType: PathType.Linear),
(pos: circle1.Position, pathType: PathType.LINEAR),
(pos: circle2.Position, pathType: null)));
AddStep("undo", () => Editor.Undo());
@ -73,11 +73,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
var controlPoints = slider.Path.ControlPoints;
(Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints.Count + 2];
args[0] = (circle1.Position, PathType.Linear);
args[0] = (circle1.Position, PathType.LINEAR);
for (int i = 0; i < controlPoints.Count; i++)
{
args[i + 1] = (controlPoints[i].Position + slider.Position, i == controlPoints.Count - 1 ? PathType.Linear : controlPoints[i].Type);
args[i + 1] = (controlPoints[i].Position + slider.Position, i == controlPoints.Count - 1 ? PathType.LINEAR : controlPoints[i].Type);
}
args[^1] = (circle2.Position, null);
@ -172,7 +172,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
mergeSelection();
AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor(
(pos: circle1.Position, pathType: PathType.Linear),
(pos: circle1.Position, pathType: PathType.LINEAR),
(pos: circle2.Position, pathType: null)));
AddAssert("samples exist", sliderSampleExist);
@ -227,7 +227,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
mergeSelection();
AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor(
(pos: circle1.Position, pathType: PathType.Linear),
(pos: circle1.Position, pathType: PathType.LINEAR),
(pos: circle2.Position, pathType: null)));
}

View File

@ -0,0 +1,91 @@
// 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 System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public partial class TestSceneOpenEditorTimestampInOsu : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
[Test]
public void TestNormalSelection()
{
addStepClickLink("00:02:170 (1,2,3)");
checkSelection(() => 2_170, 1, 2, 3);
addReset();
addStepClickLink("00:04:748 (2,3,4,1,2)");
checkSelection(() => 4_748, 2, 3, 4, 1, 2);
addReset();
addStepClickLink("00:02:170 (1,1,1)");
checkSelection(() => 2_170, 1, 1, 1);
addReset();
addStepClickLink("00:02:873 (2,2,2,2)");
checkSelection(() => 2_873, 2, 2, 2, 2);
}
[Test]
public void TestUnusualSelection()
{
HitObject firstObject = null!;
AddStep("retrieve first object", () => firstObject = EditorBeatmap.HitObjects.First());
addStepClickLink("00:00:000 (0)", "invalid combo");
checkSelection(() => firstObject.StartTime);
addReset();
addStepClickLink("00:00:000 (1)", "wrong offset");
checkSelection(() => firstObject.StartTime, 1);
addReset();
addStepClickLink("00:00:956 (2,3,4)", "wrong offset");
checkSelection(() => firstObject.StartTime, 2, 3, 4);
addReset();
addStepClickLink("00:00:956 (956|1,956|2)", "mania link");
checkSelection(() => firstObject.StartTime);
}
private void addReset() => addStepClickLink("00:00:000", "reset", false);
private void addStepClickLink(string timestamp, string step = "", bool displayTimestamp = true)
{
AddStep(displayTimestamp ? $"{step} {timestamp}" : step, () => Editor.HandleTimestamp(timestamp));
AddUntilStep("wait for seek", () => EditorClock.SeekingOrStopped.Value);
}
private void checkSelection(Func<double> startTime, params int[] comboNumbers)
=> AddUntilStep($"seeked & selected {(comboNumbers.Any() ? string.Join(",", comboNumbers) : "nothing")}", () =>
{
bool checkCombos = comboNumbers.Any()
? hasCombosInOrder(EditorBeatmap.SelectedHitObjects, comboNumbers)
: !EditorBeatmap.SelectedHitObjects.Any();
return EditorClock.CurrentTime == startTime()
&& EditorBeatmap.SelectedHitObjects.Count == comboNumbers.Length
&& checkCombos;
});
private bool hasCombosInOrder(IEnumerable<HitObject> selected, params int[] comboNumbers)
{
List<HitObject> hitObjects = selected.ToList();
if (hitObjects.Count != comboNumbers.Length)
return false;
return !hitObjects.Select(x => (OsuHitObject)x)
.Where((x, i) => x.IndexInCurrentCombo + 1 != comboNumbers[i])
.Any();
}
}
}

View File

@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
PathControlPoint[] points =
{
new PathControlPoint(new Vector2(0), PathType.PerfectCurve),
new PathControlPoint(new Vector2(0), PathType.PERFECT_CURVE),
new PathControlPoint(new Vector2(-100, 0)),
new PathControlPoint(new Vector2(100, 20))
};

View File

@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
createVisualiser(true);
addControlPointStep(new Vector2(200), PathType.Bezier);
addControlPointStep(new Vector2(200), PathType.BEZIER);
addControlPointStep(new Vector2(300));
addControlPointStep(new Vector2(500, 300));
addControlPointStep(new Vector2(700, 200));
@ -63,9 +63,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("select control point", () => visualiser.Pieces[1].IsSelected.Value = true);
addContextMenuItemStep("Perfect curve");
assertControlPointPathType(0, PathType.Bezier);
assertControlPointPathType(1, PathType.PerfectCurve);
assertControlPointPathType(3, PathType.Bezier);
assertControlPointPathType(0, PathType.BEZIER);
assertControlPointPathType(1, PathType.PERFECT_CURVE);
assertControlPointPathType(3, PathType.BEZIER);
}
[Test]
@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
createVisualiser(true);
addControlPointStep(new Vector2(200), PathType.Bezier);
addControlPointStep(new Vector2(200), PathType.BEZIER);
addControlPointStep(new Vector2(300));
addControlPointStep(new Vector2(500, 300));
addControlPointStep(new Vector2(700, 200));
@ -83,8 +83,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("select control point", () => visualiser.Pieces[2].IsSelected.Value = true);
addContextMenuItemStep("Perfect curve");
assertControlPointPathType(0, PathType.Bezier);
assertControlPointPathType(2, PathType.PerfectCurve);
assertControlPointPathType(0, PathType.BEZIER);
assertControlPointPathType(2, PathType.PERFECT_CURVE);
assertControlPointPathType(4, null);
}
@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
createVisualiser(true);
addControlPointStep(new Vector2(200), PathType.Bezier);
addControlPointStep(new Vector2(200), PathType.BEZIER);
addControlPointStep(new Vector2(300));
addControlPointStep(new Vector2(500, 300));
addControlPointStep(new Vector2(700, 200));
@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("select control point", () => visualiser.Pieces[3].IsSelected.Value = true);
addContextMenuItemStep("Perfect curve");
assertControlPointPathType(0, PathType.Bezier);
assertControlPointPathType(0, PathType.BEZIER);
AddAssert("point 3 is not inherited", () => slider.Path.ControlPoints[3].Type != null);
}
@ -112,7 +112,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
createVisualiser(true);
addControlPointStep(new Vector2(200), PathType.Linear);
addControlPointStep(new Vector2(200), PathType.LINEAR);
addControlPointStep(new Vector2(300));
addControlPointStep(new Vector2(500, 300));
addControlPointStep(new Vector2(700, 200));
@ -123,9 +123,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("select control point", () => visualiser.Pieces[1].IsSelected.Value = true);
addContextMenuItemStep("Perfect curve");
assertControlPointPathType(0, PathType.Linear);
assertControlPointPathType(1, PathType.PerfectCurve);
assertControlPointPathType(3, PathType.Linear);
assertControlPointPathType(0, PathType.LINEAR);
assertControlPointPathType(1, PathType.PERFECT_CURVE);
assertControlPointPathType(3, PathType.LINEAR);
}
[Test]
@ -133,21 +133,45 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
createVisualiser(true);
addControlPointStep(new Vector2(200), PathType.Bezier);
addControlPointStep(new Vector2(300), PathType.PerfectCurve);
addControlPointStep(new Vector2(200), PathType.BEZIER);
addControlPointStep(new Vector2(300), PathType.PERFECT_CURVE);
addControlPointStep(new Vector2(500, 300));
addControlPointStep(new Vector2(700, 200), PathType.Bezier);
addControlPointStep(new Vector2(700, 200), PathType.BEZIER);
addControlPointStep(new Vector2(500, 100));
moveMouseToControlPoint(3);
AddStep("select control point", () => visualiser.Pieces[3].IsSelected.Value = true);
addContextMenuItemStep("Inherit");
assertControlPointPathType(0, PathType.Bezier);
assertControlPointPathType(1, PathType.Bezier);
assertControlPointPathType(0, PathType.BEZIER);
assertControlPointPathType(1, PathType.BEZIER);
assertControlPointPathType(3, null);
}
[Test]
public void TestCatmullAvailableIffSelectionContainsCatmull()
{
createVisualiser(true);
addControlPointStep(new Vector2(200), PathType.CATMULL);
addControlPointStep(new Vector2(300));
addControlPointStep(new Vector2(500, 300));
addControlPointStep(new Vector2(700, 200));
addControlPointStep(new Vector2(500, 100));
moveMouseToControlPoint(2);
AddStep("select first and third control point", () =>
{
visualiser.Pieces[0].IsSelected.Value = true;
visualiser.Pieces[2].IsSelected.Value = true;
});
addContextMenuItemStep("Catmull");
assertControlPointPathType(0, PathType.CATMULL);
assertControlPointPathType(2, PathType.CATMULL);
assertControlPointPathType(4, null);
}
private void createVisualiser(bool allowSelection) => AddStep("create visualiser", () => Child = visualiser = new PathControlPointVisualiser<Slider>(slider, allowSelection)
{
Anchor = Anchor.Centre,
@ -158,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
private void addControlPointStep(Vector2 position, PathType? type)
{
AddStep($"add {type} control point at {position}", () =>
AddStep($"add {type?.Type} control point at {position}", () =>
{
slider.Path.ControlPoints.Add(new PathControlPoint(position, type));
});

View File

@ -38,9 +38,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
Position = new Vector2(256, 192),
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
new PathControlPoint(Vector2.Zero, PathType.PERFECT_CURVE),
new PathControlPoint(new Vector2(150, 150)),
new PathControlPoint(new Vector2(300, 0), PathType.PerfectCurve),
new PathControlPoint(new Vector2(300, 0), PathType.PERFECT_CURVE),
new PathControlPoint(new Vector2(400, 0)),
new PathControlPoint(new Vector2(400, 150))
})
@ -182,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
assertControlPointPosition(1, new Vector2(150, 50));
assertControlPointType(0, PathType.PerfectCurve);
assertControlPointType(0, PathType.PERFECT_CURVE);
}
[Test]
@ -210,7 +210,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("three control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece<Slider>>().Count(piece => piece.IsSelected.Value) == 3);
assertControlPointPosition(2, new Vector2(450, 50));
assertControlPointType(2, PathType.PerfectCurve);
assertControlPointType(2, PathType.PERFECT_CURVE);
assertControlPointPosition(3, new Vector2(550, 50));
@ -249,7 +249,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("slider moved", () => Precision.AlmostEquals(slider.Position, new Vector2(256, 192) + new Vector2(150, 50)));
assertControlPointPosition(0, Vector2.Zero);
assertControlPointType(0, PathType.PerfectCurve);
assertControlPointType(0, PathType.PERFECT_CURVE);
assertControlPointPosition(1, new Vector2(0, 100));
@ -272,7 +272,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
assertControlPointPosition(1, new Vector2(400, 0.01f));
assertControlPointType(0, PathType.Bezier);
assertControlPointType(0, PathType.BEZIER);
}
[Test]
@ -282,13 +282,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
addMovementStep(new Vector2(400, 0.01f));
assertControlPointType(0, PathType.Bezier);
assertControlPointType(0, PathType.BEZIER);
addMovementStep(new Vector2(150, 50));
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
assertControlPointPosition(1, new Vector2(150, 50));
assertControlPointType(0, PathType.PerfectCurve);
assertControlPointType(0, PathType.PERFECT_CURVE);
}
[Test]
@ -298,32 +298,32 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
addMovementStep(new Vector2(350, 0.01f));
assertControlPointType(2, PathType.Bezier);
assertControlPointType(2, PathType.BEZIER);
addMovementStep(new Vector2(150, 150));
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
assertControlPointPosition(4, new Vector2(150, 150));
assertControlPointType(2, PathType.PerfectCurve);
assertControlPointType(2, PathType.PERFECT_CURVE);
}
[Test]
public void TestDragControlPointPathAfterChangingType()
{
AddStep("change type to bezier", () => slider.Path.ControlPoints[2].Type = PathType.Bezier);
AddStep("change type to bezier", () => slider.Path.ControlPoints[2].Type = PathType.BEZIER);
AddStep("add point", () => slider.Path.ControlPoints.Add(new PathControlPoint(new Vector2(500, 10))));
AddStep("change type to perfect", () => slider.Path.ControlPoints[3].Type = PathType.PerfectCurve);
AddStep("change type to perfect", () => slider.Path.ControlPoints[3].Type = PathType.PERFECT_CURVE);
moveMouseToControlPoint(4);
AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
assertControlPointType(3, PathType.PerfectCurve);
assertControlPointType(3, PathType.PERFECT_CURVE);
addMovementStep(new Vector2(350, 0.01f));
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
assertControlPointPosition(4, new Vector2(350, 0.01f));
assertControlPointType(3, PathType.Bezier);
assertControlPointType(3, PathType.BEZIER);
}
private void addMovementStep(Vector2 relativePosition)

View File

@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
PathControlPoint[] points =
{
new PathControlPoint(new Vector2(0), PathType.Linear),
new PathControlPoint(new Vector2(0), PathType.LINEAR),
new PathControlPoint(new Vector2(100, 0)),
};
@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
PathControlPoint[] points =
{
new PathControlPoint(new Vector2(0), PathType.Linear),
new PathControlPoint(new Vector2(0), PathType.LINEAR),
new PathControlPoint(new Vector2(100, 0)),
};
@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
PathControlPoint[] points =
{
new PathControlPoint(new Vector2(0), PathType.PerfectCurve),
new PathControlPoint(new Vector2(0), PathType.PERFECT_CURVE),
new PathControlPoint(new Vector2(100, 0)),
new PathControlPoint(new Vector2(0, 10))
};
@ -165,7 +165,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
PathControlPoint[] points =
{
new PathControlPoint(new Vector2(0), PathType.Linear),
new PathControlPoint(new Vector2(0), PathType.LINEAR),
new PathControlPoint(new Vector2(0, 50)),
new PathControlPoint(new Vector2(0, 100))
};

View File

@ -1,8 +1,7 @@
// 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 disable
using System;
using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Rulesets.Edit;
@ -58,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertLength(200);
assertControlPointCount(2);
assertControlPointType(0, PathType.Linear);
assertControlPointType(0, PathType.LINEAR);
}
[Test]
@ -72,7 +71,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(2);
assertControlPointType(0, PathType.Linear);
assertControlPointType(0, PathType.LINEAR);
}
[Test]
@ -90,7 +89,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointType(0, PathType.PerfectCurve);
assertControlPointType(0, PathType.PERFECT_CURVE);
}
[Test]
@ -112,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointCount(4);
assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointPosition(2, new Vector2(100, 100));
assertControlPointType(0, PathType.Bezier);
assertControlPointType(0, PathType.BEZIER);
}
[Test]
@ -131,8 +130,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointType(0, PathType.Linear);
assertControlPointType(1, PathType.Linear);
assertControlPointType(0, PathType.LINEAR);
assertControlPointType(1, PathType.LINEAR);
}
[Test]
@ -150,7 +149,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(2);
assertControlPointType(0, PathType.Linear);
assertControlPointType(0, PathType.LINEAR);
assertLength(100);
}
@ -172,7 +171,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointType(0, PathType.PerfectCurve);
assertControlPointType(0, PathType.PERFECT_CURVE);
}
[Test]
@ -196,7 +195,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(4);
assertControlPointType(0, PathType.Bezier);
assertControlPointType(0, PathType.BEZIER);
}
[Test]
@ -216,8 +215,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointCount(3);
assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointPosition(2, new Vector2(100));
assertControlPointType(0, PathType.Linear);
assertControlPointType(1, PathType.Linear);
assertControlPointType(0, PathType.LINEAR);
assertControlPointType(1, PathType.LINEAR);
}
[Test]
@ -240,8 +239,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointCount(4);
assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointPosition(2, new Vector2(100));
assertControlPointType(0, PathType.Linear);
assertControlPointType(1, PathType.PerfectCurve);
assertControlPointType(0, PathType.LINEAR);
assertControlPointType(1, PathType.PERFECT_CURVE);
}
[Test]
@ -269,25 +268,79 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointPosition(2, new Vector2(100));
assertControlPointPosition(3, new Vector2(200, 100));
assertControlPointPosition(4, new Vector2(200));
assertControlPointType(0, PathType.PerfectCurve);
assertControlPointType(2, PathType.PerfectCurve);
assertControlPointType(0, PathType.PERFECT_CURVE);
assertControlPointType(2, PathType.PERFECT_CURVE);
}
[Test]
public void TestBeginPlacementWithoutReleasingMouse()
public void TestSliderDrawingDoesntActivateAfterNormalPlacement()
{
Vector2 startPoint = new Vector2(200);
addMovementStep(startPoint);
addClickStep(MouseButton.Left);
for (int i = 0; i < 20; i++)
{
if (i == 5)
AddStep("press left button", () => InputManager.PressButton(MouseButton.Left));
addMovementStep(startPoint + new Vector2(i * 40, MathF.Sin(i * MathF.PI / 5) * 50));
}
AddStep("release left button", () => InputManager.ReleaseButton(MouseButton.Left));
assertPlaced(false);
addClickStep(MouseButton.Right);
assertPlaced(true);
assertControlPointType(0, PathType.BEZIER);
}
[Test]
public void TestSliderDrawingCurve()
{
Vector2 startPoint = new Vector2(200);
addMovementStep(startPoint);
AddStep("press left button", () => InputManager.PressButton(MouseButton.Left));
for (int i = 0; i < 20; i++)
addMovementStep(startPoint + new Vector2(i * 40, MathF.Sin(i * MathF.PI / 5) * 50));
AddStep("release left button", () => InputManager.ReleaseButton(MouseButton.Left));
assertPlaced(true);
assertLength(760, tolerance: 10);
assertControlPointCount(5);
assertControlPointType(0, PathType.BSpline(3));
assertControlPointType(1, null);
assertControlPointType(2, null);
assertControlPointType(3, null);
assertControlPointType(4, null);
}
[Test]
public void TestSliderDrawingLinear()
{
addMovementStep(new Vector2(200));
AddStep("press left button", () => InputManager.PressButton(MouseButton.Left));
addMovementStep(new Vector2(300, 200));
addMovementStep(new Vector2(400, 200));
addMovementStep(new Vector2(400, 300));
addMovementStep(new Vector2(400));
addMovementStep(new Vector2(300, 400));
addMovementStep(new Vector2(200, 400));
AddStep("release left button", () => InputManager.ReleaseButton(MouseButton.Left));
addClickStep(MouseButton.Right);
assertPlaced(true);
assertLength(200);
assertControlPointCount(2);
assertControlPointType(0, PathType.Linear);
assertLength(600, tolerance: 10);
assertControlPointCount(4);
assertControlPointType(0, PathType.LINEAR);
assertControlPointType(1, null);
assertControlPointType(2, null);
assertControlPointType(3, null);
}
[Test]
@ -306,7 +359,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointType(0, PathType.Bezier);
assertControlPointType(0, PathType.BEZIER);
}
[Test]
@ -326,7 +379,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointType(0, PathType.PerfectCurve);
assertControlPointType(0, PathType.PERFECT_CURVE);
}
[Test]
@ -347,7 +400,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointType(0, PathType.PerfectCurve);
assertControlPointType(0, PathType.PERFECT_CURVE);
}
[Test]
@ -368,7 +421,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointType(0, PathType.Bezier);
assertControlPointType(0, PathType.BEZIER);
}
[Test]
@ -385,7 +438,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointType(0, PathType.PerfectCurve);
assertControlPointType(0, PathType.PERFECT_CURVE);
}
private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position)));
@ -397,16 +450,16 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
private void assertPlaced(bool expected) => AddAssert($"slider {(expected ? "placed" : "not placed")}", () => (getSlider() != null) == expected);
private void assertLength(double expected) => AddAssert($"slider length is {expected}", () => Precision.AlmostEquals(expected, getSlider().Distance, 1));
private void assertLength(double expected, double tolerance = 1) => AddAssert($"slider length is {expected}±{tolerance}", () => getSlider()!.Distance, () => Is.EqualTo(expected).Within(tolerance));
private void assertControlPointCount(int expected) => AddAssert($"has {expected} control points", () => getSlider().Path.ControlPoints.Count == expected);
private void assertControlPointCount(int expected) => AddAssert($"has {expected} control points", () => getSlider()!.Path.ControlPoints.Count, () => Is.EqualTo(expected));
private void assertControlPointType(int index, PathType type) => AddAssert($"control point {index} is {type}", () => getSlider().Path.ControlPoints[index].Type == type);
private void assertControlPointType(int index, PathType? type) => AddAssert($"control point {index} is {type?.ToString() ?? "inherit"}", () => getSlider()!.Path.ControlPoints[index].Type, () => Is.EqualTo(type));
private void assertControlPointPosition(int index, Vector2 position) =>
AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, getSlider().Path.ControlPoints[index].Position, 1));
AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, getSlider()!.Path.ControlPoints[index].Position, 1));
private Slider getSlider() => HitObjectContainer.Count > 0 ? ((DrawableSlider)HitObjectContainer[0]).HitObject : null;
private Slider? getSlider() => HitObjectContainer.Count > 0 ? ((DrawableSlider)HitObjectContainer[0]).HitObject : null;
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSlider((Slider)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new SliderPlacementBlueprint();

View File

@ -22,12 +22,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
private readonly PathControlPoint[][] paths =
{
createPathSegment(
PathType.PerfectCurve,
PathType.PERFECT_CURVE,
new Vector2(200, -50),
new Vector2(250, 0)
),
createPathSegment(
PathType.Linear,
PathType.LINEAR,
new Vector2(100, 0),
new Vector2(100, 100)
)

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
slider = new Slider
{
Position = new Vector2(256, 192),
Path = new SliderPath(PathType.Bezier, new[]
Path = new SliderPath(PathType.BEZIER, new[]
{
Vector2.Zero,
new Vector2(150, 150),

View File

@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
ControlPoints =
{
new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
new PathControlPoint(Vector2.Zero, PathType.PERFECT_CURVE),
new PathControlPoint(new Vector2(136, 205)),
new PathControlPoint(new Vector2(-4, 226))
}
@ -181,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
OsuSelectionHandler selectionHandler;
AddAssert("first control point perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve);
AddAssert("first control point perfect", () => slider.Path.ControlPoints[0].Type == PathType.PERFECT_CURVE);
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
AddStep("rotate 90 degrees ccw", () =>
@ -190,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
selectionHandler.HandleRotation(-90);
});
AddAssert("first control point still perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve);
AddAssert("first control point still perfect", () => slider.Path.ControlPoints[0].Type == PathType.PERFECT_CURVE);
}
[Test]
@ -223,7 +223,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
OsuSelectionHandler selectionHandler;
AddAssert("first control point perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve);
AddAssert("first control point perfect", () => slider.Path.ControlPoints[0].Type == PathType.PERFECT_CURVE);
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
AddStep("flip slider horizontally", () =>
@ -232,7 +232,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
selectionHandler.OnPressed(new KeyBindingPressEvent<GlobalAction>(InputManager.CurrentState, GlobalAction.EditorFlipVertically));
});
AddAssert("first control point still perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve);
AddAssert("first control point still perfect", () => slider.Path.ControlPoints[0].Type == PathType.PERFECT_CURVE);
}
[Test]

View File

@ -45,9 +45,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
Position = new Vector2(0, 50),
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
new PathControlPoint(Vector2.Zero, PathType.PERFECT_CURVE),
new PathControlPoint(new Vector2(150, 150)),
new PathControlPoint(new Vector2(300, 0), PathType.PerfectCurve),
new PathControlPoint(new Vector2(300, 0), PathType.PERFECT_CURVE),
new PathControlPoint(new Vector2(400, 0)),
new PathControlPoint(new Vector2(400, 150))
})
@ -73,20 +73,20 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("slider split", () => slider is not null && EditorBeatmap.HitObjects.Count == 2 &&
sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, EditorBeatmap.HitObjects[1].StartTime - split_gap,
(new Vector2(0, 50), PathType.PerfectCurve),
(new Vector2(0, 50), PathType.PERFECT_CURVE),
(new Vector2(150, 200), null),
(new Vector2(300, 50), null)
) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[1], slider.StartTime, endTime + split_gap,
(new Vector2(300, 50), PathType.PerfectCurve),
(new Vector2(300, 50), PathType.PERFECT_CURVE),
(new Vector2(400, 50), null),
(new Vector2(400, 200), null)
));
AddStep("undo", () => Editor.Undo());
AddAssert("original slider restored", () => EditorBeatmap.HitObjects.Count == 1 && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, endTime,
(new Vector2(0, 50), PathType.PerfectCurve),
(new Vector2(0, 50), PathType.PERFECT_CURVE),
(new Vector2(150, 200), null),
(new Vector2(300, 50), PathType.PerfectCurve),
(new Vector2(300, 50), PathType.PERFECT_CURVE),
(new Vector2(400, 50), null),
(new Vector2(400, 200), null)
));
@ -104,11 +104,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
Position = new Vector2(0, 50),
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
new PathControlPoint(Vector2.Zero, PathType.PERFECT_CURVE),
new PathControlPoint(new Vector2(150, 150)),
new PathControlPoint(new Vector2(300, 0), PathType.Bezier),
new PathControlPoint(new Vector2(300, 0), PathType.BEZIER),
new PathControlPoint(new Vector2(400, 0)),
new PathControlPoint(new Vector2(400, 150), PathType.Catmull),
new PathControlPoint(new Vector2(400, 150), PathType.CATMULL),
new PathControlPoint(new Vector2(300, 200)),
new PathControlPoint(new Vector2(400, 250))
})
@ -139,15 +139,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("slider split", () => slider is not null && EditorBeatmap.HitObjects.Count == 3 &&
sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, EditorBeatmap.HitObjects[1].StartTime - split_gap,
(new Vector2(0, 50), PathType.PerfectCurve),
(new Vector2(0, 50), PathType.PERFECT_CURVE),
(new Vector2(150, 200), null),
(new Vector2(300, 50), null)
) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[1], EditorBeatmap.HitObjects[0].GetEndTime() + split_gap, slider.StartTime - split_gap,
(new Vector2(300, 50), PathType.Bezier),
(new Vector2(300, 50), PathType.BEZIER),
(new Vector2(400, 50), null),
(new Vector2(400, 200), null)
) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[2], EditorBeatmap.HitObjects[1].GetEndTime() + split_gap, endTime + split_gap * 2,
(new Vector2(400, 200), PathType.Catmull),
(new Vector2(400, 200), PathType.CATMULL),
(new Vector2(300, 250), null),
(new Vector2(400, 300), null)
));
@ -165,9 +165,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
Position = new Vector2(0, 50),
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
new PathControlPoint(Vector2.Zero, PathType.PERFECT_CURVE),
new PathControlPoint(new Vector2(150, 150)),
new PathControlPoint(new Vector2(300, 0), PathType.PerfectCurve),
new PathControlPoint(new Vector2(300, 0), PathType.PERFECT_CURVE),
new PathControlPoint(new Vector2(400, 0)),
new PathControlPoint(new Vector2(400, 150))
})

View File

@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
PathControlPoint[] points =
{
new PathControlPoint(new Vector2(0), PathType.Linear),
new PathControlPoint(new Vector2(0), PathType.LINEAR),
new PathControlPoint(new Vector2(100, 0)),
};

View File

@ -81,12 +81,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
new Slider
{
StartTime = 3200,
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), })
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), })
},
new Slider
{
StartTime = 5200,
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), })
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), })
}
}
},
@ -105,12 +105,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
new Slider
{
StartTime = 1000,
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), })
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), })
},
new Slider
{
StartTime = 4000,
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), })
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), })
},
}
},
@ -140,7 +140,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
{
StartTime = 3000,
Position = new Vector2(156, 242),
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(200, 0), })
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(200, 0), })
},
new Spinner
{

View File

@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
var slider = new Slider
{
StartTime = 1000,
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), })
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), })
};
CreateHitObjectTest(new HitObjectTestData(slider), shouldMiss);

View File

@ -26,9 +26,9 @@ namespace osu.Game.Rulesets.Osu.Tests
{
ControlPoints =
{
new PathControlPoint(new Vector2(), PathType.Linear),
new PathControlPoint(new Vector2(-64, -128), PathType.Linear), // absolute position: (64, 0)
new PathControlPoint(new Vector2(-128, 0), PathType.Linear) // absolute position: (0, 128)
new PathControlPoint(new Vector2(), PathType.LINEAR),
new PathControlPoint(new Vector2(-64, -128), PathType.LINEAR), // absolute position: (64, 0)
new PathControlPoint(new Vector2(-128, 0), PathType.LINEAR) // absolute position: (0, 128)
}
},
RepeatCount = 1

View File

@ -167,7 +167,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = Time.Current + 500,
Position = new Vector2(250),
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(0, 100),

View File

@ -264,7 +264,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_slider,
Position = positionSlider,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(50, 0),
@ -308,7 +308,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_slider,
Position = positionSlider,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(50, 0),
@ -391,7 +391,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_slider,
Position = positionSlider,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(25, 0),
@ -428,7 +428,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_first_slider,
Position = positionFirstSlider,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(25, 0),
@ -438,7 +438,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_second_slider,
Position = positionSecondSlider,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(25, 0),
@ -521,7 +521,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_first_slider,
Position = positionFirstSlider,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(25, 0),
@ -531,7 +531,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_second_slider,
Position = positionSecondSlider,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(25, 0),
@ -571,7 +571,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_first_slider,
Position = positionFirstSlider,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(25, 0),
@ -581,7 +581,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_second_slider,
Position = positionSecondSlider,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(25, 0),

View File

@ -219,7 +219,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = Time.Current + time_offset,
Position = new Vector2(239, 176),
Path = new SliderPath(PathType.PerfectCurve, new[]
Path = new SliderPath(PathType.PERFECT_CURVE, new[]
{
Vector2.Zero,
new Vector2(154, 28),
@ -255,7 +255,7 @@ namespace osu.Game.Rulesets.Osu.Tests
SliderVelocityMultiplier = speedMultiplier,
StartTime = Time.Current + time_offset,
Position = new Vector2(0, -(distance / 2)),
Path = new SliderPath(PathType.PerfectCurve, new[]
Path = new SliderPath(PathType.PERFECT_CURVE, new[]
{
Vector2.Zero,
new Vector2(0, distance),
@ -273,7 +273,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = Time.Current + time_offset,
Position = new Vector2(-max_length / 2, 0),
Path = new SliderPath(PathType.PerfectCurve, new[]
Path = new SliderPath(PathType.PERFECT_CURVE, new[]
{
Vector2.Zero,
new Vector2(max_length / 2, max_length / 2),
@ -293,7 +293,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = Time.Current + time_offset,
Position = new Vector2(-max_length / 2, 0),
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(max_length * 0.375f, max_length * 0.18f),
@ -316,7 +316,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = Time.Current + time_offset,
Position = new Vector2(-max_length / 2, 0),
Path = new SliderPath(PathType.Bezier, new[]
Path = new SliderPath(PathType.BEZIER, new[]
{
Vector2.Zero,
new Vector2(max_length * 0.375f, max_length * 0.18f),
@ -338,7 +338,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = Time.Current + time_offset,
Position = new Vector2(0, 0),
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(-max_length / 2, 0),
@ -365,7 +365,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = Time.Current + time_offset,
Position = new Vector2(-max_length / 4, 0),
Path = new SliderPath(PathType.Catmull, new[]
Path = new SliderPath(PathType.CATMULL, new[]
{
Vector2.Zero,
new Vector2(max_length * 0.125f, max_length * 0.125f),

View File

@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Tests
Position = new Vector2(256, 192),
IndexInCurrentCombo = 0,
StartTime = Time.Current,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(150, 100),
@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Tests
Position = new Vector2(256, 192),
ComboIndex = 1,
StartTime = dho.HitObject.StartTime,
Path = new SliderPath(PathType.Bezier, new[]
Path = new SliderPath(PathType.BEZIER, new[]
{
Vector2.Zero,
new Vector2(150, 100),
@ -80,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Tests
Position = new Vector2(256, 192),
IndexInCurrentCombo = 0,
StartTime = Time.Current,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(150, 100),

View File

@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Tests
StartTime = time_slider_start,
Position = new Vector2(0, 0),
SliderVelocityMultiplier = velocity,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(followCircleRadius, 0),

View File

@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Tests
Position = new Vector2(0, 0),
SliderVelocityMultiplier = 10f,
RepeatCount = repeatCount,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(sliderLength, 0),
@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Tests
Position = new Vector2(0, 0),
SliderVelocityMultiplier = 10f,
RepeatCount = repeatCount,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(sliderLength, 0),
@ -145,7 +145,7 @@ namespace osu.Game.Rulesets.Osu.Tests
StartTime = time_slider_start,
Position = new Vector2(0, 0),
SliderVelocityMultiplier = 10f,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(slider_path_length * 10, 0),
@ -488,7 +488,7 @@ namespace osu.Game.Rulesets.Osu.Tests
StartTime = time_slider_start,
Position = new Vector2(0, 0),
SliderVelocityMultiplier = 0.1f,
Path = new SliderPath(PathType.PerfectCurve, new[]
Path = new SliderPath(PathType.PERFECT_CURVE, new[]
{
Vector2.Zero,
new Vector2(slider_path_length, 0),

View File

@ -217,7 +217,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = 3000,
Position = new Vector2(100, 100),
Path = new SliderPath(PathType.PerfectCurve, new[]
Path = new SliderPath(PathType.PERFECT_CURVE, new[]
{
Vector2.Zero,
new Vector2(300, 200)
@ -227,7 +227,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = 13000,
Position = new Vector2(100, 100),
Path = new SliderPath(PathType.PerfectCurve, new[]
Path = new SliderPath(PathType.PERFECT_CURVE, new[]
{
Vector2.Zero,
new Vector2(300, 200)
@ -238,7 +238,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = 23000,
Position = new Vector2(100, 100),
Path = new SliderPath(PathType.PerfectCurve, new[]
Path = new SliderPath(PathType.PERFECT_CURVE, new[]
{
Vector2.Zero,
new Vector2(300, 200)

View File

@ -196,7 +196,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_slider,
Position = positionSlider,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(25, 0),
@ -238,7 +238,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_slider,
Position = positionSlider,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(25, 0),
@ -318,7 +318,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_slider,
Position = positionSlider,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(25, 0),
@ -352,7 +352,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_first_slider,
Position = positionFirstSlider,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(25, 0),
@ -362,7 +362,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_second_slider,
Position = positionSecondSlider,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(25, 0),

View File

@ -95,7 +95,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
if (ShouldSerializeFlashlightRating())
if (ShouldSerializeFlashlightDifficulty())
yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty);
yield return (ATTRIB_ID_SLIDER_FACTOR, SliderFactor);
@ -128,7 +128,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// unless the fields are also renamed.
[UsedImplicitly]
public bool ShouldSerializeFlashlightRating() => Mods.Any(m => m is ModFlashlight);
public bool ShouldSerializeFlashlightDifficulty() => Mods.Any(m => m is ModFlashlight);
#endregion
}

View File

@ -221,11 +221,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
/// </summary>
private void updatePathType()
{
if (ControlPoint.Type != PathType.PerfectCurve)
if (ControlPoint.Type != PathType.PERFECT_CURVE)
return;
if (PointsInSegment.Count > 3)
ControlPoint.Type = PathType.Bezier;
ControlPoint.Type = PathType.BEZIER;
if (PointsInSegment.Count != 3)
return;
@ -233,7 +233,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
ReadOnlySpan<Vector2> points = PointsInSegment.Select(p => p.Position).ToArray();
RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points);
if (boundingBox.Width >= 640 || boundingBox.Height >= 480)
ControlPoint.Type = PathType.Bezier;
ControlPoint.Type = PathType.BEZIER;
}
/// <summary>
@ -256,18 +256,22 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private Color4 getColourFromNodeType()
{
if (!(ControlPoint.Type is PathType pathType))
if (ControlPoint.Type is not PathType pathType)
return colours.Yellow;
switch (pathType)
switch (pathType.Type)
{
case PathType.Catmull:
case SplineType.Catmull:
return colours.SeaFoam;
case PathType.Bezier:
return colours.Pink;
case SplineType.BSpline:
if (!pathType.Degree.HasValue)
return colours.PinkLighter;
case PathType.PerfectCurve:
int idx = Math.Clamp(pathType.Degree.Value, 0, 3);
return new[] { colours.PinkDarker, colours.PinkDark, colours.Pink, colours.PinkLight }[idx];
case SplineType.PerfectCurve:
return colours.PurpleDark;
default:
@ -275,6 +279,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
}
}
public LocalisableString TooltipText => ControlPoint.Type.ToString() ?? string.Empty;
public LocalisableString TooltipText => ControlPoint.Type?.Description ?? string.Empty;
}
}

View File

@ -242,18 +242,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
int indexInSegment = piece.PointsInSegment.IndexOf(piece.ControlPoint);
switch (type)
if (type?.Type == SplineType.PerfectCurve)
{
case PathType.PerfectCurve:
// Can't always create a circular arc out of 4 or more points,
// so we split the segment into one 3-point circular arc segment
// and one segment of the previous type.
int thirdPointIndex = indexInSegment + 2;
// Can't always create a circular arc out of 4 or more points,
// so we split the segment into one 3-point circular arc segment
// and one segment of the previous type.
int thirdPointIndex = indexInSegment + 2;
if (piece.PointsInSegment.Count > thirdPointIndex + 1)
piece.PointsInSegment[thirdPointIndex].Type = piece.PointsInSegment[0].Type;
break;
if (piece.PointsInSegment.Count > thirdPointIndex + 1)
piece.PointsInSegment[thirdPointIndex].Type = piece.PointsInSegment[0].Type;
}
hitObject.Path.ExpectedDistance.Value = null;
@ -367,13 +364,19 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
List<MenuItem> curveTypeItems = new List<MenuItem>();
if (!selectedPieces.Contains(Pieces[0]))
{
curveTypeItems.Add(createMenuItemForPathType(null));
curveTypeItems.Add(new OsuMenuItemSpacer());
}
// todo: hide/disable items which aren't valid for selected points
curveTypeItems.Add(createMenuItemForPathType(PathType.Linear));
curveTypeItems.Add(createMenuItemForPathType(PathType.PerfectCurve));
curveTypeItems.Add(createMenuItemForPathType(PathType.Bezier));
curveTypeItems.Add(createMenuItemForPathType(PathType.Catmull));
curveTypeItems.Add(createMenuItemForPathType(PathType.LINEAR));
curveTypeItems.Add(createMenuItemForPathType(PathType.PERFECT_CURVE));
curveTypeItems.Add(createMenuItemForPathType(PathType.BEZIER));
curveTypeItems.Add(createMenuItemForPathType(PathType.BSpline(3)));
if (selectedPieces.Any(piece => piece.ControlPoint.Type?.Type == SplineType.Catmull))
curveTypeItems.Add(createMenuItemForPathType(PathType.CATMULL));
var menuItems = new List<MenuItem>
{
@ -405,7 +408,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
int totalCount = Pieces.Count(p => p.IsSelected.Value);
int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type == type);
var item = new TernaryStateRadioMenuItem(type == null ? "Inherit" : type.ToString().Humanize(), MenuItemType.Standard, _ =>
var item = new TernaryStateRadioMenuItem(type?.Description ?? "Inherit", MenuItemType.Standard, _ =>
{
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
updatePathType(p, type);

View File

@ -3,6 +3,7 @@
#nullable disable
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
@ -10,6 +11,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@ -44,6 +46,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
[Resolved(CanBeNull = true)]
private IDistanceSnapProvider distanceSnapProvider { get; set; }
[Resolved(CanBeNull = true)]
private FreehandSliderToolboxGroup freehandToolboxGroup { get; set; }
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder();
protected override bool IsValidForPlacement => HitObject.Path.HasValidLength;
public SliderPlacementBlueprint()
@ -51,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
RelativeSizeAxes = Axes.Both;
HitObject.Path.ControlPoints.Add(segmentStart = new PathControlPoint(Vector2.Zero, PathType.Linear));
HitObject.Path.ControlPoints.Add(segmentStart = new PathControlPoint(Vector2.Zero, PathType.LINEAR));
currentSegmentLength = 1;
}
@ -66,13 +73,28 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
controlPointVisualiser = new PathControlPointVisualiser<Slider>(HitObject, false)
};
setState(SliderPlacementState.Initial);
state = SliderPlacementState.Initial;
}
protected override void LoadComplete()
{
base.LoadComplete();
inputManager = GetContainingInputManager();
if (freehandToolboxGroup != null)
{
freehandToolboxGroup.Tolerance.BindValueChanged(e =>
{
bSplineBuilder.Tolerance = e.NewValue;
Scheduler.AddOnce(updateSliderPathFromBSplineBuilder);
}, true);
freehandToolboxGroup.CornerThreshold.BindValueChanged(e =>
{
bSplineBuilder.CornerThreshold = e.NewValue;
Scheduler.AddOnce(updateSliderPathFromBSplineBuilder);
}, true);
}
}
[Resolved]
@ -87,8 +109,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
case SliderPlacementState.Initial:
BeginPlacement();
double? nearestSliderVelocity = (editorBeatmap.HitObjects
.LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime) as Slider)?.SliderVelocityMultiplier;
double? nearestSliderVelocity = (editorBeatmap
.HitObjects
.LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime) as Slider)?.SliderVelocityMultiplier;
HitObject.SliderVelocityMultiplier = nearestSliderVelocity ?? 1;
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
@ -98,7 +121,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
ApplyDefaultsToHitObject();
break;
case SliderPlacementState.Body:
case SliderPlacementState.ControlPoints:
updateCursor();
break;
}
@ -115,7 +138,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
beginCurve();
break;
case SliderPlacementState.Body:
case SliderPlacementState.ControlPoints:
if (canPlaceNewControlPoint(out var lastPoint))
{
// Place a new point by detatching the current cursor.
@ -128,7 +151,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
Debug.Assert(lastPoint != null);
segmentStart = lastPoint;
segmentStart.Type = PathType.Linear;
segmentStart.Type = PathType.LINEAR;
currentSegmentLength = 1;
}
@ -139,25 +162,50 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
return true;
}
protected override bool OnDragStart(DragStartEvent e)
{
if (e.Button != MouseButton.Left)
return base.OnDragStart(e);
if (state != SliderPlacementState.ControlPoints)
return base.OnDragStart(e);
// Only enter drawing mode if no additional control points have been placed.
int controlPointCount = HitObject.Path.ControlPoints.Count;
if (controlPointCount > 2 || (controlPointCount == 2 && HitObject.Path.ControlPoints.Last() != cursor))
return base.OnDragStart(e);
bSplineBuilder.AddLinearPoint(ToLocalSpace(e.ScreenSpaceMouseDownPosition) - HitObject.Position);
state = SliderPlacementState.Drawing;
return true;
}
protected override void OnDrag(DragEvent e)
{
base.OnDrag(e);
if (state == SliderPlacementState.Drawing)
{
bSplineBuilder.AddLinearPoint(ToLocalSpace(e.ScreenSpaceMousePosition) - HitObject.Position);
Scheduler.AddOnce(updateSliderPathFromBSplineBuilder);
}
}
protected override void OnDragEnd(DragEndEvent e)
{
base.OnDragEnd(e);
if (state == SliderPlacementState.Drawing)
endCurve();
}
protected override void OnMouseUp(MouseUpEvent e)
{
if (state == SliderPlacementState.Body && e.Button == MouseButton.Right)
if (state == SliderPlacementState.ControlPoints && e.Button == MouseButton.Right)
endCurve();
base.OnMouseUp(e);
}
private void beginCurve()
{
BeginPlacement(commitStart: true);
setState(SliderPlacementState.Body);
}
private void endCurve()
{
updateSlider();
EndPlacement(true);
}
protected override void Update()
{
base.Update();
@ -167,21 +215,39 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
updatePathType();
}
private void beginCurve()
{
BeginPlacement(commitStart: true);
state = SliderPlacementState.ControlPoints;
}
private void endCurve()
{
updateSlider();
EndPlacement(true);
}
private void updatePathType()
{
if (state == SliderPlacementState.Drawing)
{
segmentStart.Type = PathType.BSpline(3);
return;
}
switch (currentSegmentLength)
{
case 1:
case 2:
segmentStart.Type = PathType.Linear;
segmentStart.Type = PathType.LINEAR;
break;
case 3:
segmentStart.Type = PathType.PerfectCurve;
segmentStart.Type = PathType.PERFECT_CURVE;
break;
default:
segmentStart.Type = PathType.Bezier;
segmentStart.Type = PathType.BEZIER;
break;
}
}
@ -195,13 +261,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
HitObject.Path.ControlPoints.Add(cursor = new PathControlPoint { Position = Vector2.Zero });
// The path type should be adjusted in the progression of updatePathType() (Linear -> PC -> Bezier).
// The path type should be adjusted in the progression of updatePathType() (LINEAR -> PC -> BEZIER).
currentSegmentLength++;
updatePathType();
}
// Update the cursor position.
var result = positionSnapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.Body ? SnapType.GlobalGrids : SnapType.All);
var result = positionSnapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.ControlPoints ? SnapType.GlobalGrids : SnapType.All);
cursor.Position = ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position;
}
else if (cursor != null)
@ -210,7 +276,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
HitObject.Path.ControlPoints.Remove(cursor);
cursor = null;
// The path type should be adjusted in the reverse progression of updatePathType() (Bezier -> PC -> Linear).
// The path type should be adjusted in the reverse progression of updatePathType() (BEZIER -> PC -> LINEAR).
currentSegmentLength--;
updatePathType();
}
@ -240,15 +306,55 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
tailCirclePiece.UpdateFrom(HitObject.TailCircle);
}
private void setState(SliderPlacementState newState)
private void updateSliderPathFromBSplineBuilder()
{
state = newState;
IReadOnlyList<Vector2> builderPoints = bSplineBuilder.ControlPoints;
if (builderPoints.Count == 0)
return;
int lastSegmentStart = 0;
PathType? lastPathType = null;
HitObject.Path.ControlPoints.Clear();
// Iterate through generated points, finding each segment and adding non-inheriting path types where appropriate.
// Importantly, the B-Spline builder returns three Vector2s at the same location when a new segment is to be started.
for (int i = 0; i < builderPoints.Count; i++)
{
bool isLastPoint = i == builderPoints.Count - 1;
bool isNewSegment = i < builderPoints.Count - 2 && builderPoints[i] == builderPoints[i + 1] && builderPoints[i] == builderPoints[i + 2];
if (isNewSegment || isLastPoint)
{
int pointsInSegment = i - lastSegmentStart;
// Where possible, we can use the simpler LINEAR path type.
PathType? pathType = pointsInSegment == 1 ? PathType.LINEAR : PathType.BSpline(3);
// Linear segments can be combined, as two adjacent linear sections are computationally the same as one with the points combined.
if (lastPathType == pathType && lastPathType == PathType.LINEAR)
pathType = null;
HitObject.Path.ControlPoints.Add(new PathControlPoint(builderPoints[lastSegmentStart], pathType));
for (int j = lastSegmentStart + 1; j < i; j++)
HitObject.Path.ControlPoints.Add(new PathControlPoint(builderPoints[j]));
if (isLastPoint)
HitObject.Path.ControlPoints.Add(new PathControlPoint(builderPoints[i]));
// Skip the redundant duplicated points (see isNewSegment above) which have been coalesced into a path type.
lastSegmentStart = (i += 2);
if (pathType != null) lastPathType = pathType;
}
}
}
private enum SliderPlacementState
{
Initial,
Body,
ControlPoints,
Drawing
}
}
}

View File

@ -0,0 +1,100 @@
// 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.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
namespace osu.Game.Rulesets.Osu.Edit
{
public partial class FreehandSliderToolboxGroup : EditorToolboxGroup
{
public FreehandSliderToolboxGroup()
: base("slider")
{
}
public BindableFloat Tolerance { get; } = new BindableFloat(1.5f)
{
MinValue = 0.05f,
MaxValue = 3f,
Precision = 0.01f
};
public BindableFloat CornerThreshold { get; } = new BindableFloat(0.4f)
{
MinValue = 0.05f,
MaxValue = 1f,
Precision = 0.01f
};
// We map internal ranges to a more standard range of values for display to the user.
private readonly BindableInt displayTolerance = new BindableInt(40)
{
MinValue = 5,
MaxValue = 100
};
private readonly BindableInt displayCornerThreshold = new BindableInt(40)
{
MinValue = 5,
MaxValue = 100
};
private ExpandableSlider<int> toleranceSlider = null!;
private ExpandableSlider<int> cornerThresholdSlider = null!;
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
toleranceSlider = new ExpandableSlider<int>
{
Current = displayTolerance
},
cornerThresholdSlider = new ExpandableSlider<int>
{
Current = displayCornerThreshold
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
displayTolerance.BindValueChanged(tolerance =>
{
toleranceSlider.ContractedLabelText = $"C. P. S.: {tolerance.NewValue:N0}";
toleranceSlider.ExpandedLabelText = $"Control Point Spacing: {tolerance.NewValue:N0}";
Tolerance.Value = displayToInternalTolerance(tolerance.NewValue);
}, true);
displayCornerThreshold.BindValueChanged(threshold =>
{
cornerThresholdSlider.ContractedLabelText = $"C. T.: {threshold.NewValue:N0}";
cornerThresholdSlider.ExpandedLabelText = $"Corner Threshold: {threshold.NewValue:N0}";
CornerThreshold.Value = displayToInternalCornerThreshold(threshold.NewValue);
}, true);
Tolerance.BindValueChanged(tolerance =>
displayTolerance.Value = internalToDisplayTolerance(tolerance.NewValue)
);
CornerThreshold.BindValueChanged(threshold =>
displayCornerThreshold.Value = internalToDisplayCornerThreshold(threshold.NewValue)
);
float displayToInternalTolerance(float v) => v / 33f;
int internalToDisplayTolerance(float v) => (int)Math.Round(v * 33f);
float displayToInternalCornerThreshold(float v) => v / 100f;
int internalToDisplayCornerThreshold(float v) => (int)Math.Round(v * 100f);
}
}
}

View File

@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
@ -63,6 +64,9 @@ namespace osu.Game.Rulesets.Osu.Edit
[Cached(typeof(IDistanceSnapProvider))]
protected readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider();
[Cached]
protected readonly FreehandSliderToolboxGroup FreehandlSliderToolboxGroup = new FreehandSliderToolboxGroup();
[BackgroundDependencyLoader]
private void load()
{
@ -94,10 +98,12 @@ namespace osu.Game.Rulesets.Osu.Edit
// we may be entering the screen with a selection already active
updateDistanceSnapGrid();
RightToolbox.Add(new TransformToolboxGroup
{
RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler
});
RightToolbox.AddRange(new EditorToolboxGroup[]
{
new TransformToolboxGroup { RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler, },
FreehandlSliderToolboxGroup
}
);
}
protected override ComposeBlueprintContainer CreateBlueprintContainer()
@ -106,6 +112,34 @@ namespace osu.Game.Rulesets.Osu.Edit
public override string ConvertSelectionToString()
=> string.Join(',', selectedHitObjects.Cast<OsuHitObject>().OrderBy(h => h.StartTime).Select(h => (h.IndexInCurrentCombo + 1).ToString()));
// 1,2,3,4 ...
private static readonly Regex selection_regex = new Regex(@"^\d+(,\d+)*$", RegexOptions.Compiled);
public override void SelectFromTimestamp(double timestamp, string objectDescription)
{
if (!selection_regex.IsMatch(objectDescription))
return;
List<OsuHitObject> remainingHitObjects = EditorBeatmap.HitObjects.Cast<OsuHitObject>().Where(h => h.StartTime >= timestamp).ToList();
string[] splitDescription = objectDescription.Split(',').ToArray();
for (int i = 0; i < splitDescription.Length; i++)
{
if (!int.TryParse(splitDescription[i], out int combo) || combo < 1)
continue;
OsuHitObject current = remainingHitObjects.FirstOrDefault(h => h.IndexInCurrentCombo + 1 == combo);
if (current == null)
continue;
EditorBeatmap.SelectedHitObjects.Add(current);
if (i < splitDescription.Length - 1)
remainingHitObjects = remainingHitObjects.Where(h => h != current && h.StartTime >= current.StartTime).ToList();
}
}
private DistanceSnapGrid distanceSnapGrid;
private Container distanceSnapGridContainer;

View File

@ -321,7 +321,7 @@ namespace osu.Game.Rulesets.Osu.Edit
if (mergedHitObject.Path.ControlPoints.Count == 0)
{
mergedHitObject.Path.ControlPoints.Add(new PathControlPoint(Vector2.Zero, PathType.Linear));
mergedHitObject.Path.ControlPoints.Add(new PathControlPoint(Vector2.Zero, PathType.LINEAR));
}
// Merge all the selected hit objects into one slider path.
@ -351,7 +351,7 @@ namespace osu.Game.Rulesets.Osu.Edit
// Turn the last control point into a linear type if this is the first merging circle in a sequence, so the subsequent control points can be inherited path type.
if (!lastCircle)
{
mergedHitObject.Path.ControlPoints.Last().Type = PathType.Linear;
mergedHitObject.Path.ControlPoints.Last().Type = PathType.LINEAR;
}
mergedHitObject.Path.ControlPoints.Add(new PathControlPoint(selectedMergeableObject.Position - mergedHitObject.Position));

View File

@ -48,6 +48,8 @@ namespace osu.Game.Rulesets.Osu
public override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor();
public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new OsuHealthProcessor(drainStartTime);
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new OsuBeatmapConverter(beatmap, this);
public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new OsuBeatmapProcessor(beatmap);

View File

@ -339,6 +339,11 @@ namespace osu.Game.Rulesets.Osu.Replays
AddFrameToReplay(startFrame);
// 0.05 rad/ms, or ~477 RPM, as per stable.
// the redundant conversion from RPM to rad/ms is here for ease of testing custom SPM specs.
const float spin_rpm = 0.05f / (2 * MathF.PI) * 60000;
float radsPerMillisecond = MathUtils.DegreesToRadians(spin_rpm * 360) / 60000;
switch (h)
{
// We add intermediate frames for spinning / following a slider here.
@ -354,7 +359,7 @@ namespace osu.Game.Rulesets.Osu.Replays
for (double nextFrame = h.StartTime + GetFrameDelay(h.StartTime); nextFrame < spinner.EndTime; nextFrame += GetFrameDelay(nextFrame))
{
t = ApplyModsToTimeDelta(previousFrame, nextFrame) * spinnerDirection;
angle += (float)t / 20;
angle += (float)t * radsPerMillisecond;
Vector2 pos = SPINNER_CENTRE + CirclePosition(angle, SPIN_RADIUS);
AddFrameToReplay(new OsuReplayFrame((int)nextFrame, new Vector2(pos.X, pos.Y), action));
@ -363,7 +368,7 @@ namespace osu.Game.Rulesets.Osu.Replays
}
t = ApplyModsToTimeDelta(previousFrame, spinner.EndTime) * spinnerDirection;
angle += (float)t / 20;
angle += (float)t * radsPerMillisecond;
Vector2 endPosition = SPINNER_CENTRE + CirclePosition(angle, SPIN_RADIUS);

View File

@ -0,0 +1,215 @@
// 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 System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Scoring
{
/// <summary>
/// Reference implementation for osu!stable's HP drain.
/// Cannot be used for gameplay.
/// </summary>
public partial class LegacyOsuHealthProcessor : DrainingHealthProcessor
{
private const double hp_bar_maximum = 200;
private const double hp_combo_geki = 14;
private const double hp_hit_300 = 6;
private const double hp_slider_repeat = 4;
private const double hp_slider_tick = 3;
public Action<string>? OnIterationFail;
public Action<string>? OnIterationSuccess;
public bool ApplyComboEndBonus { get; set; } = true;
private double lowestHpEver;
private double lowestHpEnd;
private double lowestHpComboEnd;
private double hpRecoveryAvailable;
private double hpMultiplierNormal;
private double hpMultiplierComboEnd;
public LegacyOsuHealthProcessor(double drainStartTime)
: base(drainStartTime)
{
}
public override void ApplyBeatmap(IBeatmap beatmap)
{
lowestHpEver = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 195, 160, 60);
lowestHpComboEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 198, 170, 80);
lowestHpEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 198, 180, 80);
hpRecoveryAvailable = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 8, 4, 0);
base.ApplyBeatmap(beatmap);
}
protected override void ApplyResultInternal(JudgementResult result)
{
if (!IsSimulating)
throw new NotSupportedException("The legacy osu! health processor is not supported for gameplay.");
}
protected override void RevertResultInternal(JudgementResult result)
{
if (!IsSimulating)
throw new NotSupportedException("The legacy osu! health processor is not supported for gameplay.");
}
protected override void Reset(bool storeResults)
{
hpMultiplierNormal = 1;
hpMultiplierComboEnd = 1;
base.Reset(storeResults);
}
protected override double ComputeDrainRate()
{
double testDrop = 0.05;
double currentHp;
double currentHpUncapped;
do
{
currentHp = hp_bar_maximum;
currentHpUncapped = hp_bar_maximum;
double lowestHp = currentHp;
double lastTime = DrainStartTime;
int currentBreak = 0;
bool fail = false;
int comboTooLowCount = 0;
string failReason = string.Empty;
for (int i = 0; i < Beatmap.HitObjects.Count; i++)
{
HitObject h = Beatmap.HitObjects[i];
// Find active break (between current and lastTime)
double localLastTime = lastTime;
double breakTime = 0;
// Subtract any break time from the duration since the last object
if (Beatmap.Breaks.Count > 0 && currentBreak < Beatmap.Breaks.Count)
{
BreakPeriod e = Beatmap.Breaks[currentBreak];
if (e.StartTime >= localLastTime && e.EndTime <= h.StartTime)
{
// consider break start equal to object end time for version 8+ since drain stops during this time
breakTime = (Beatmap.BeatmapInfo.BeatmapVersion < 8) ? (e.EndTime - e.StartTime) : e.EndTime - localLastTime;
currentBreak++;
}
}
reduceHp(testDrop * (h.StartTime - lastTime - breakTime));
lastTime = h.GetEndTime();
if (currentHp < lowestHp)
lowestHp = currentHp;
if (currentHp <= lowestHpEver)
{
fail = true;
testDrop *= 0.96;
failReason = $"hp too low ({currentHp / hp_bar_maximum} < {lowestHpEver / hp_bar_maximum})";
break;
}
double hpReduction = testDrop * (h.GetEndTime() - h.StartTime);
double hpOverkill = Math.Max(0, hpReduction - currentHp);
reduceHp(hpReduction);
if (h is Slider slider)
{
for (int j = 0; j < slider.RepeatCount + 2; j++)
increaseHp(hpMultiplierNormal * hp_slider_repeat);
foreach (var _ in slider.NestedHitObjects.OfType<SliderTick>())
increaseHp(hpMultiplierNormal * hp_slider_tick);
}
else if (h is Spinner spinner)
{
foreach (var _ in spinner.NestedHitObjects.Where(t => t is not SpinnerBonusTick))
increaseHp(hpMultiplierNormal * 1.7);
}
if (hpOverkill > 0 && currentHp - hpOverkill <= lowestHpEver)
{
fail = true;
testDrop *= 0.96;
failReason = $"overkill ({currentHp / hp_bar_maximum} - {hpOverkill / hp_bar_maximum} <= {lowestHpEver / hp_bar_maximum})";
break;
}
if (ApplyComboEndBonus && (i == Beatmap.HitObjects.Count - 1 || ((OsuHitObject)Beatmap.HitObjects[i + 1]).NewCombo))
{
increaseHp(hpMultiplierComboEnd * hp_combo_geki + hpMultiplierNormal * hp_hit_300);
if (currentHp < lowestHpComboEnd)
{
if (++comboTooLowCount > 2)
{
hpMultiplierComboEnd *= 1.07;
hpMultiplierNormal *= 1.03;
fail = true;
failReason = $"combo end hp too low ({currentHp / hp_bar_maximum} < {lowestHpComboEnd / hp_bar_maximum})";
break;
}
}
}
else
increaseHp(hpMultiplierNormal * hp_hit_300);
}
if (!fail && currentHp < lowestHpEnd)
{
fail = true;
testDrop *= 0.94;
hpMultiplierComboEnd *= 1.01;
hpMultiplierNormal *= 1.01;
failReason = $"end hp too low ({currentHp / hp_bar_maximum} < {lowestHpEnd / hp_bar_maximum})";
}
double recovery = (currentHpUncapped - hp_bar_maximum) / Beatmap.HitObjects.Count;
if (!fail && recovery < hpRecoveryAvailable)
{
fail = true;
testDrop *= 0.96;
hpMultiplierComboEnd *= 1.02;
hpMultiplierNormal *= 1.01;
failReason = $"recovery too low ({recovery / hp_bar_maximum} < {hpRecoveryAvailable / hp_bar_maximum})";
}
if (fail)
{
OnIterationFail?.Invoke($"FAILED drop {testDrop / hp_bar_maximum}: {failReason}");
continue;
}
OnIterationSuccess?.Invoke($"PASSED drop {testDrop / hp_bar_maximum}");
return testDrop / hp_bar_maximum;
} while (true);
void reduceHp(double amount)
{
currentHpUncapped = Math.Max(0, currentHpUncapped - amount);
currentHp = Math.Max(0, currentHp - amount);
}
void increaseHp(double amount)
{
currentHpUncapped += amount;
currentHp = Math.Max(0, Math.Min(hp_bar_maximum, currentHp + amount));
}
}
}
}

View File

@ -0,0 +1,223 @@
// 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 System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Scoring
{
public partial class OsuHealthProcessor : DrainingHealthProcessor
{
public Action<string>? OnIterationFail;
public Action<string>? OnIterationSuccess;
private double lowestHpEver;
private double lowestHpEnd;
private double hpRecoveryAvailable;
private double hpMultiplierNormal;
public OsuHealthProcessor(double drainStartTime)
: base(drainStartTime)
{
}
public override void ApplyBeatmap(IBeatmap beatmap)
{
lowestHpEver = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.975, 0.8, 0.3);
lowestHpEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.99, 0.9, 0.4);
hpRecoveryAvailable = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.04, 0.02, 0);
base.ApplyBeatmap(beatmap);
}
protected override void Reset(bool storeResults)
{
hpMultiplierNormal = 1;
base.Reset(storeResults);
}
protected override double ComputeDrainRate()
{
double testDrop = 0.00025;
double currentHp;
double currentHpUncapped;
while (true)
{
currentHp = 1;
currentHpUncapped = 1;
double lowestHp = currentHp;
double lastTime = DrainStartTime;
int currentBreak = 0;
bool fail = false;
for (int i = 0; i < Beatmap.HitObjects.Count; i++)
{
HitObject h = Beatmap.HitObjects[i];
// Find active break (between current and lastTime)
double localLastTime = lastTime;
double breakTime = 0;
// TODO: This doesn't handle overlapping/sequential breaks correctly (/b/614).
// Subtract any break time from the duration since the last object
// Note that this method is a bit convoluted, but matches stable code for compatibility.
if (Beatmap.Breaks.Count > 0 && currentBreak < Beatmap.Breaks.Count)
{
BreakPeriod e = Beatmap.Breaks[currentBreak];
if (e.StartTime >= localLastTime && e.EndTime <= h.StartTime)
{
// consider break start equal to object end time for version 8+ since drain stops during this time
breakTime = (Beatmap.BeatmapInfo.BeatmapVersion < 8) ? (e.EndTime - e.StartTime) : e.EndTime - localLastTime;
currentBreak++;
}
}
reduceHp(testDrop * (h.StartTime - lastTime - breakTime));
lastTime = h.GetEndTime();
if (currentHp < lowestHp)
lowestHp = currentHp;
if (currentHp <= lowestHpEver)
{
fail = true;
testDrop *= 0.96;
OnIterationFail?.Invoke($"FAILED drop {testDrop}: hp too low ({currentHp} < {lowestHpEver})");
break;
}
double hpReduction = testDrop * (h.GetEndTime() - h.StartTime);
double hpOverkill = Math.Max(0, hpReduction - currentHp);
reduceHp(hpReduction);
switch (h)
{
case Slider slider:
{
foreach (var nested in slider.NestedHitObjects)
increaseHp(nested);
break;
}
case Spinner spinner:
{
foreach (var nested in spinner.NestedHitObjects.Where(t => t is not SpinnerBonusTick))
increaseHp(nested);
break;
}
}
// Note: Because HP is capped during the above increases, long sliders (with many ticks) or spinners
// will appear to overkill at lower drain levels than they should. However, it is also not correct to simply use the uncapped version.
if (hpOverkill > 0 && currentHp - hpOverkill <= lowestHpEver)
{
fail = true;
testDrop *= 0.96;
OnIterationFail?.Invoke($"FAILED drop {testDrop}: overkill ({currentHp} - {hpOverkill} <= {lowestHpEver})");
break;
}
increaseHp(h);
}
if (!fail && currentHp < lowestHpEnd)
{
fail = true;
testDrop *= 0.94;
hpMultiplierNormal *= 1.01;
OnIterationFail?.Invoke($"FAILED drop {testDrop}: end hp too low ({currentHp} < {lowestHpEnd})");
}
double recovery = (currentHpUncapped - 1) / Beatmap.HitObjects.Count;
if (!fail && recovery < hpRecoveryAvailable)
{
fail = true;
testDrop *= 0.96;
hpMultiplierNormal *= 1.01;
OnIterationFail?.Invoke($"FAILED drop {testDrop}: recovery too low ({recovery} < {hpRecoveryAvailable})");
}
if (!fail)
{
OnIterationSuccess?.Invoke($"PASSED drop {testDrop}");
return testDrop;
}
}
void reduceHp(double amount)
{
currentHpUncapped = Math.Max(0, currentHpUncapped - amount);
currentHp = Math.Max(0, currentHp - amount);
}
void increaseHp(HitObject hitObject)
{
double amount = healthIncreaseFor(hitObject, hitObject.CreateJudgement().MaxResult);
currentHpUncapped += amount;
currentHp = Math.Max(0, Math.Min(1, currentHp + amount));
}
}
protected override double GetHealthIncreaseFor(JudgementResult result) => healthIncreaseFor(result.HitObject, result.Type);
private double healthIncreaseFor(HitObject hitObject, HitResult result)
{
double increase = 0;
switch (result)
{
case HitResult.SmallTickMiss:
return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.02, -0.075, -0.14);
case HitResult.LargeTickMiss:
return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.02, -0.075, -0.14);
case HitResult.Miss:
return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.03, -0.125, -0.2);
case HitResult.SmallTickHit:
// This result always comes from the slider tail, which is judged the same as a repeat.
increase = 0.02;
break;
case HitResult.LargeTickHit:
// This result comes from either a slider tick or repeat.
increase = hitObject is SliderTick ? 0.015 : 0.02;
break;
case HitResult.Meh:
increase = 0.002;
break;
case HitResult.Ok:
increase = 0.011;
break;
case HitResult.Great:
increase = 0.03;
break;
case HitResult.SmallBonus:
increase = 0.0085;
break;
case HitResult.LargeBonus:
increase = 0.01;
break;
}
return hpMultiplierNormal * increase;
}
}
}

View File

@ -88,9 +88,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
protected override void OnSliderTick()
{
this.ScaleTo(DrawableSliderBall.FOLLOW_AREA * 1.08f, 40, Easing.OutQuint)
.Then()
.ScaleTo(DrawableSliderBall.FOLLOW_AREA, 200f, Easing.OutQuint);
if (Scale.X >= DrawableSliderBall.FOLLOW_AREA * 0.98f)
{
this.ScaleTo(DrawableSliderBall.FOLLOW_AREA * 1.08f, 40, Easing.OutQuint)
.Then()
.ScaleTo(DrawableSliderBall.FOLLOW_AREA, 200f, Easing.OutQuint);
}
}
protected override void OnSliderBreak()

View File

@ -59,9 +59,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
protected override void OnSliderTick()
{
this.ScaleTo(DrawableSliderBall.FOLLOW_AREA * 1.08f, 40, Easing.OutQuint)
.Then()
.ScaleTo(DrawableSliderBall.FOLLOW_AREA, 200f, Easing.OutQuint);
if (Scale.X >= DrawableSliderBall.FOLLOW_AREA * 0.98f)
{
this.ScaleTo(DrawableSliderBall.FOLLOW_AREA * 1.08f, 40, Easing.OutQuint)
.Then()
.ScaleTo(DrawableSliderBall.FOLLOW_AREA, 200f, Easing.OutQuint);
}
}
protected override void OnSliderBreak()

View File

@ -44,8 +44,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
protected override void OnSliderTick()
{
this.ScaleTo(2.2f)
.ScaleTo(2f, 200);
if (Scale.X >= 2f)
{
this.ScaleTo(2.2f)
.ScaleTo(2f, 200);
}
}
protected override void OnSliderBreak()

View File

@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
double IHasDistance.Distance => Duration * Velocity;
SliderPath IHasPath.Path
=> new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(1) }, ((IHasDistance)this).Distance / LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER);
=> new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(1) }, ((IHasDistance)this).Distance / LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER);
#endregion
}

View File

@ -664,7 +664,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
assertObjectHasBanks(hitObjects[9], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_NORMAL);
}
void assertObjectHasBanks(HitObject hitObject, string normalBank, string? additionsBank = null)
static void assertObjectHasBanks(HitObject hitObject, string normalBank, string? additionsBank = null)
{
Assert.AreEqual(normalBank, hitObject.Samples[0].Bank);
@ -809,14 +809,14 @@ namespace osu.Game.Tests.Beatmaps.Formats
var first = ((IHasPath)decoded.HitObjects[0]).Path;
Assert.That(first.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
Assert.That(first.ControlPoints[0].Type, Is.EqualTo(PathType.PerfectCurve));
Assert.That(first.ControlPoints[0].Type, Is.EqualTo(PathType.PERFECT_CURVE));
Assert.That(first.ControlPoints[1].Position, Is.EqualTo(new Vector2(161, -244)));
Assert.That(first.ControlPoints[1].Type, Is.EqualTo(null));
// ReSharper disable once HeuristicUnreachableCode
// weird one, see https://youtrack.jetbrains.com/issue/RIDER-70159.
Assert.That(first.ControlPoints[2].Position, Is.EqualTo(new Vector2(376, -3)));
Assert.That(first.ControlPoints[2].Type, Is.EqualTo(PathType.Bezier));
Assert.That(first.ControlPoints[2].Type, Is.EqualTo(PathType.BEZIER));
Assert.That(first.ControlPoints[3].Position, Is.EqualTo(new Vector2(68, 15)));
Assert.That(first.ControlPoints[3].Type, Is.EqualTo(null));
Assert.That(first.ControlPoints[4].Position, Is.EqualTo(new Vector2(259, -132)));
@ -828,7 +828,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var second = ((IHasPath)decoded.HitObjects[1]).Path;
Assert.That(second.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
Assert.That(second.ControlPoints[0].Type, Is.EqualTo(PathType.PerfectCurve));
Assert.That(second.ControlPoints[0].Type, Is.EqualTo(PathType.PERFECT_CURVE));
Assert.That(second.ControlPoints[1].Position, Is.EqualTo(new Vector2(161, -244)));
Assert.That(second.ControlPoints[1].Type, Is.EqualTo(null));
Assert.That(second.ControlPoints[2].Position, Is.EqualTo(new Vector2(376, -3)));
@ -838,14 +838,14 @@ namespace osu.Game.Tests.Beatmaps.Formats
var third = ((IHasPath)decoded.HitObjects[2]).Path;
Assert.That(third.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
Assert.That(third.ControlPoints[0].Type, Is.EqualTo(PathType.Bezier));
Assert.That(third.ControlPoints[0].Type, Is.EqualTo(PathType.BEZIER));
Assert.That(third.ControlPoints[1].Position, Is.EqualTo(new Vector2(0, 192)));
Assert.That(third.ControlPoints[1].Type, Is.EqualTo(null));
Assert.That(third.ControlPoints[2].Position, Is.EqualTo(new Vector2(224, 192)));
Assert.That(third.ControlPoints[2].Type, Is.EqualTo(null));
Assert.That(third.ControlPoints[3].Position, Is.EqualTo(new Vector2(224, 0)));
Assert.That(third.ControlPoints[3].Type, Is.EqualTo(PathType.Bezier));
Assert.That(third.ControlPoints[3].Type, Is.EqualTo(PathType.BEZIER));
Assert.That(third.ControlPoints[4].Position, Is.EqualTo(new Vector2(224, -192)));
Assert.That(third.ControlPoints[4].Type, Is.EqualTo(null));
Assert.That(third.ControlPoints[5].Position, Is.EqualTo(new Vector2(480, -192)));
@ -857,7 +857,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var fourth = ((IHasPath)decoded.HitObjects[3]).Path;
Assert.That(fourth.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
Assert.That(fourth.ControlPoints[0].Type, Is.EqualTo(PathType.Bezier));
Assert.That(fourth.ControlPoints[0].Type, Is.EqualTo(PathType.BEZIER));
Assert.That(fourth.ControlPoints[1].Position, Is.EqualTo(new Vector2(1, 1)));
Assert.That(fourth.ControlPoints[1].Type, Is.EqualTo(null));
Assert.That(fourth.ControlPoints[2].Position, Is.EqualTo(new Vector2(2, 2)));
@ -871,7 +871,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var fifth = ((IHasPath)decoded.HitObjects[4]).Path;
Assert.That(fifth.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
Assert.That(fifth.ControlPoints[0].Type, Is.EqualTo(PathType.Bezier));
Assert.That(fifth.ControlPoints[0].Type, Is.EqualTo(PathType.BEZIER));
Assert.That(fifth.ControlPoints[1].Position, Is.EqualTo(new Vector2(1, 1)));
Assert.That(fifth.ControlPoints[1].Type, Is.EqualTo(null));
Assert.That(fifth.ControlPoints[2].Position, Is.EqualTo(new Vector2(2, 2)));
@ -882,7 +882,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(fifth.ControlPoints[4].Type, Is.EqualTo(null));
Assert.That(fifth.ControlPoints[5].Position, Is.EqualTo(new Vector2(4, 4)));
Assert.That(fifth.ControlPoints[5].Type, Is.EqualTo(PathType.Bezier));
Assert.That(fifth.ControlPoints[5].Type, Is.EqualTo(PathType.BEZIER));
Assert.That(fifth.ControlPoints[6].Position, Is.EqualTo(new Vector2(5, 5)));
Assert.That(fifth.ControlPoints[6].Type, Is.EqualTo(null));
@ -890,12 +890,12 @@ namespace osu.Game.Tests.Beatmaps.Formats
var sixth = ((IHasPath)decoded.HitObjects[5]).Path;
Assert.That(sixth.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
Assert.That(sixth.ControlPoints[0].Type == PathType.Bezier);
Assert.That(sixth.ControlPoints[0].Type == PathType.BEZIER);
Assert.That(sixth.ControlPoints[1].Position, Is.EqualTo(new Vector2(75, 145)));
Assert.That(sixth.ControlPoints[1].Type == null);
Assert.That(sixth.ControlPoints[2].Position, Is.EqualTo(new Vector2(170, 75)));
Assert.That(sixth.ControlPoints[2].Type == PathType.Bezier);
Assert.That(sixth.ControlPoints[2].Type == PathType.BEZIER);
Assert.That(sixth.ControlPoints[3].Position, Is.EqualTo(new Vector2(300, 145)));
Assert.That(sixth.ControlPoints[3].Type == null);
Assert.That(sixth.ControlPoints[4].Position, Is.EqualTo(new Vector2(410, 20)));
@ -905,12 +905,12 @@ namespace osu.Game.Tests.Beatmaps.Formats
var seventh = ((IHasPath)decoded.HitObjects[6]).Path;
Assert.That(seventh.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
Assert.That(seventh.ControlPoints[0].Type == PathType.PerfectCurve);
Assert.That(seventh.ControlPoints[0].Type == PathType.PERFECT_CURVE);
Assert.That(seventh.ControlPoints[1].Position, Is.EqualTo(new Vector2(75, 145)));
Assert.That(seventh.ControlPoints[1].Type == null);
Assert.That(seventh.ControlPoints[2].Position, Is.EqualTo(new Vector2(170, 75)));
Assert.That(seventh.ControlPoints[2].Type == PathType.PerfectCurve);
Assert.That(seventh.ControlPoints[2].Type == PathType.PERFECT_CURVE);
Assert.That(seventh.ControlPoints[3].Position, Is.EqualTo(new Vector2(300, 145)));
Assert.That(seventh.ControlPoints[3].Type == null);
Assert.That(seventh.ControlPoints[4].Position, Is.EqualTo(new Vector2(410, 20)));
@ -1017,7 +1017,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
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));
Assert.That(controlPoints.Single(c => c.Type != null).Type, Is.EqualTo(PathType.CATMULL));
}
}
@ -1033,9 +1033,9 @@ namespace osu.Game.Tests.Beatmaps.Formats
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[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);
}
}
@ -1052,7 +1052,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
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[0].Type, Is.EqualTo(PathType.CATMULL));
Assert.That(controlPoints[0].Position, Is.EqualTo(Vector2.Zero));
Assert.That(controlPoints[1].Type, Is.Null);
Assert.That(controlPoints[1].Position, Is.Not.EqualTo(Vector2.Zero));

View File

@ -77,7 +77,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
compareBeatmaps(decoded, decodedAfterEncode);
ControlPointInfo removeLegacyControlPointTypes(ControlPointInfo controlPointInfo)
static ControlPointInfo removeLegacyControlPointTypes(ControlPointInfo controlPointInfo)
{
// emulate non-legacy control points by cloning the non-legacy portion.
// the assertion is that the encoder can recreate this losslessly from hitobject data.
@ -113,6 +113,33 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.IsTrue(areComboColoursEqual(expected.skin.Configuration, actual.skin.Configuration));
}
[Test]
public void TestEncodeBSplineCurveType()
{
var beatmap = new Beatmap
{
HitObjects =
{
new Slider
{
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero, PathType.BSpline(3)),
new PathControlPoint(new Vector2(50)),
new PathControlPoint(new Vector2(100), PathType.BSpline(3)),
new PathControlPoint(new Vector2(150))
})
},
}
};
var decodedAfterEncode = decodeFromLegacy(encodeToLegacy((beatmap, new TestLegacySkin(beatmaps_resource_store, string.Empty))), string.Empty);
var decodedSlider = (Slider)decodedAfterEncode.beatmap.HitObjects[0];
Assert.That(decodedSlider.Path.ControlPoints.Count, Is.EqualTo(4));
Assert.That(decodedSlider.Path.ControlPoints[0].Type, Is.EqualTo(PathType.BSpline(3)));
Assert.That(decodedSlider.Path.ControlPoints[2].Type, Is.EqualTo(PathType.BSpline(3)));
}
[Test]
public void TestEncodeMultiSegmentSliderWithFloatingPointError()
{
@ -125,10 +152,10 @@ namespace osu.Game.Tests.Beatmaps.Formats
Position = new Vector2(0.6f),
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero, PathType.Bezier),
new PathControlPoint(Vector2.Zero, PathType.BEZIER),
new PathControlPoint(new Vector2(0.5f)),
new PathControlPoint(new Vector2(0.51f)), // This is actually on the same position as the previous one in legacy beatmaps (truncated to int).
new PathControlPoint(new Vector2(1f), PathType.Bezier),
new PathControlPoint(new Vector2(1f), PathType.BEZIER),
new PathControlPoint(new Vector2(2f))
})
},
@ -174,8 +201,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
private class TestLegacySkin : LegacySkin
{
public TestLegacySkin(IResourceStore<byte[]> storage, string fileName)
: base(new SkinInfo { Name = "Test Skin", Creator = "Craftplacer" }, null, storage, fileName)
public TestLegacySkin(IResourceStore<byte[]> fallbackStore, string fileName)
: base(new SkinInfo { Name = "Test Skin", Creator = "Craftplacer" }, null, fallbackStore, fileName)
{
}
}

View File

@ -162,7 +162,7 @@ namespace osu.Game.Tests.Editing
{
new PathControlPoint(Vector2.Zero),
new PathControlPoint(Vector2.One),
new PathControlPoint(new Vector2(2), PathType.Bezier),
new PathControlPoint(new Vector2(2), PathType.BEZIER),
new PathControlPoint(new Vector2(3)),
}, 50)
},
@ -179,7 +179,7 @@ namespace osu.Game.Tests.Editing
StartTime = 2000,
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero, PathType.Bezier),
new PathControlPoint(Vector2.Zero, PathType.BEZIER),
new PathControlPoint(new Vector2(4)),
new PathControlPoint(new Vector2(5)),
}, 100)

View File

@ -20,7 +20,7 @@ namespace osu.Game.Tests.NonVisual.Ranking
public void TestDistributedHits()
{
var events = Enumerable.Range(-5, 11)
.Select(t => new HitEvent(t - 5, HitResult.Great, new HitObject(), null, null));
.Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null));
var unstableRate = new UnstableRate(events);
@ -33,14 +33,46 @@ namespace osu.Game.Tests.NonVisual.Ranking
{
var events = new[]
{
new HitEvent(-100, HitResult.Miss, new HitObject(), null, null),
new HitEvent(0, HitResult.Great, new HitObject(), null, null),
new HitEvent(200, HitResult.Meh, new HitObject { HitWindows = HitWindows.Empty }, null, null),
new HitEvent(-100, 1.0, HitResult.Miss, new HitObject(), null, null),
new HitEvent(0, 1.0, HitResult.Great, new HitObject(), null, null),
new HitEvent(200, 1.0, HitResult.Meh, new HitObject { HitWindows = HitWindows.Empty }, null, null),
};
var unstableRate = new UnstableRate(events);
Assert.AreEqual(0, unstableRate.Value);
}
[Test]
public void TestStaticRateChange()
{
var events = new[]
{
new HitEvent(-150, 1.5, HitResult.Great, new HitObject(), null, null),
new HitEvent(-150, 1.5, HitResult.Great, new HitObject(), null, null),
new HitEvent(150, 1.5, HitResult.Great, new HitObject(), null, null),
new HitEvent(150, 1.5, HitResult.Great, new HitObject(), null, null),
};
var unstableRate = new UnstableRate(events);
Assert.AreEqual(10 * 100, unstableRate.Value);
}
[Test]
public void TestDynamicRateChange()
{
var events = new[]
{
new HitEvent(-50, 0.5, HitResult.Great, new HitObject(), null, null),
new HitEvent(75, 0.75, HitResult.Great, new HitObject(), null, null),
new HitEvent(-100, 1.0, HitResult.Great, new HitObject(), null, null),
new HitEvent(125, 1.25, HitResult.Great, new HitObject(), null, null),
};
var unstableRate = new UnstableRate(events);
Assert.AreEqual(10 * 100, unstableRate.Value);
}
}
}

View File

@ -10,14 +10,17 @@ using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring.Legacy;
using osu.Game.Tests.Beatmaps;
@ -117,6 +120,35 @@ namespace osu.Game.Tests.Rulesets.Scoring
Assert.That(scoreProcessor.GetDisplayScore(scoringMode), Is.EqualTo(expectedScore).Within(0.5d));
}
[TestCase(typeof(OsuRuleset))]
[TestCase(typeof(TaikoRuleset))]
[TestCase(typeof(CatchRuleset))]
[TestCase(typeof(ManiaRuleset))]
public void TestBeatmapWithALotOfObjectsDoesNotOverflowClassicScore(Type rulesetType)
{
const int object_count = 999999;
var ruleset = (Ruleset)Activator.CreateInstance(rulesetType)!;
scoreProcessor = new ScoreProcessor(ruleset);
var largeBeatmap = new TestBeatmap(ruleset.RulesetInfo)
{
HitObjects = new List<HitObject>(Enumerable.Repeat(new TestHitObject(HitResult.Great), object_count))
};
scoreProcessor.ApplyBeatmap(largeBeatmap);
for (int i = 0; i < object_count; ++i)
{
var judgementResult = new JudgementResult(largeBeatmap.HitObjects[i], largeBeatmap.HitObjects[i].CreateJudgement())
{
Type = HitResult.Great
};
scoreProcessor.ApplyResult(judgementResult);
}
Assert.That(scoreProcessor.GetDisplayScore(ScoringMode.Classic), Is.GreaterThan(0));
}
[Test]
public void TestEmptyBeatmap(
[Values(ScoringMode.Standardised, ScoringMode.Classic)]

View File

@ -149,8 +149,8 @@ namespace osu.Game.Tests.Skins
private class TestSkin : Skin
{
public TestSkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore<byte[]>? storage = null, string configurationFilename = "skin.ini")
: base(skin, resources, storage, configurationFilename)
public TestSkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore<byte[]>? fallbackStore = null, string configurationFilename = "skin.ini")
: base(skin, resources, fallbackStore, configurationFilename)
{
}

View File

@ -95,8 +95,8 @@ namespace osu.Game.Tests.Skins
{
public const string SAMPLE_NAME = "test-sample";
public TestSkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore<byte[]>? storage = null, string configurationFilename = "skin.ini")
: base(skin, resources, storage, configurationFilename)
public TestSkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore<byte[]>? fallbackStore = null, string configurationFilename = "skin.ini")
: base(skin, resources, fallbackStore, configurationFilename)
{
}

View File

@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual.Editing
ControlPoints =
{
new PathControlPoint(),
new PathControlPoint(new Vector2(100, 0), PathType.Bezier)
new PathControlPoint(new Vector2(100, 0), PathType.BEZIER)
}
}
};

View File

@ -34,51 +34,51 @@ namespace osu.Game.Tests.Visual.Editing
{
new MenuItem("File")
{
Items = new[]
Items = new OsuMenuItem[]
{
new EditorMenuItem("Clear All Notes"),
new EditorMenuItem("Open Difficulty..."),
new EditorMenuItem("Save"),
new EditorMenuItem("Create a new Difficulty..."),
new EditorMenuItemSpacer(),
new OsuMenuItemSpacer(),
new EditorMenuItem("Revert to Saved"),
new EditorMenuItem("Revert to Saved (Full)"),
new EditorMenuItemSpacer(),
new OsuMenuItemSpacer(),
new EditorMenuItem("Test Beatmap"),
new EditorMenuItem("Open AiMod"),
new EditorMenuItemSpacer(),
new OsuMenuItemSpacer(),
new EditorMenuItem("Upload Beatmap..."),
new EditorMenuItem("Export Package"),
new EditorMenuItem("Export Map Package"),
new EditorMenuItem("Import from..."),
new EditorMenuItemSpacer(),
new OsuMenuItemSpacer(),
new EditorMenuItem("Open Song Folder"),
new EditorMenuItem("Open .osu in Notepad"),
new EditorMenuItem("Open .osb in Notepad"),
new EditorMenuItemSpacer(),
new OsuMenuItemSpacer(),
new EditorMenuItem("Exit"),
}
},
new MenuItem("Timing")
{
Items = new[]
Items = new OsuMenuItem[]
{
new EditorMenuItem("Time Signature"),
new EditorMenuItem("Metronome Clicks"),
new EditorMenuItemSpacer(),
new OsuMenuItemSpacer(),
new EditorMenuItem("Add Timing Section"),
new EditorMenuItem("Add Inheriting Section"),
new EditorMenuItem("Reset Current Section"),
new EditorMenuItem("Delete Timing Section"),
new EditorMenuItem("Resnap Current Section"),
new EditorMenuItemSpacer(),
new OsuMenuItemSpacer(),
new EditorMenuItem("Timing Setup"),
new EditorMenuItemSpacer(),
new OsuMenuItemSpacer(),
new EditorMenuItem("Resnap All Notes", MenuItemType.Destructive),
new EditorMenuItem("Move all notes in time...", MenuItemType.Destructive),
new EditorMenuItem("Recalculate Slider Lengths", MenuItemType.Destructive),
new EditorMenuItem("Delete All Timing Sections", MenuItemType.Destructive),
new EditorMenuItemSpacer(),
new OsuMenuItemSpacer(),
new EditorMenuItem("Set Current Position as Preview Point"),
}
},

View File

@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Editing
new Slider
{
Position = new Vector2(128, 256),
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(216, 0),

View File

@ -0,0 +1,151 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Localisation;
using osu.Game.Online.Chat;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Select;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.Editing
{
public partial class TestSceneOpenEditorTimestamp : OsuGameTestScene
{
private Editor editor => (Editor)Game.ScreenStack.CurrentScreen;
private EditorBeatmap editorBeatmap => editor.ChildrenOfType<EditorBeatmap>().Single();
private EditorClock editorClock => editor.ChildrenOfType<EditorClock>().Single();
[Test]
public void TestErrorNotifications()
{
RulesetInfo rulesetInfo = new OsuRuleset().RulesetInfo;
addStepClickLink("00:00:000", waitForSeek: false);
AddUntilStep("received 'must be in edit'",
() => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEditorToHandleLinks),
() => Is.EqualTo(1));
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo.Invoke());
AddUntilStep("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
addStepClickLink("00:00:000 (1)", waitForSeek: false);
AddUntilStep("received 'must be in edit'",
() => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEditorToHandleLinks),
() => Is.EqualTo(2));
setUpEditor(rulesetInfo);
AddAssert("ruleset is osu!", () => editorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo));
addStepClickLink("00:000", "invalid link", waitForSeek: false);
AddUntilStep("received 'failed to process'",
() => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToParseEditorLink),
() => Is.EqualTo(1));
addStepClickLink("50000:00:000", "too long link", waitForSeek: false);
AddUntilStep("received 'failed to process'",
() => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToParseEditorLink),
() => Is.EqualTo(2));
}
[Test]
public void TestHandleCurrentScreenChanges()
{
RulesetInfo rulesetInfo = new OsuRuleset().RulesetInfo;
setUpEditor(rulesetInfo);
AddAssert("is osu! ruleset", () => editorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo));
addStepClickLink("100:00:000", "long link");
AddUntilStep("moved to end of track", () => editorClock.CurrentTime, () => Is.EqualTo(editorClock.TrackLength));
addStepScreenModeTo(EditorScreenMode.SongSetup);
addStepClickLink("00:00:000");
assertOnScreenAt(EditorScreenMode.SongSetup, 0);
addStepClickLink("00:05:000 (0|0)");
assertMovedScreenTo(EditorScreenMode.Compose);
addStepScreenModeTo(EditorScreenMode.Design);
addStepClickLink("00:10:000");
assertOnScreenAt(EditorScreenMode.Design, 10_000);
addStepClickLink("00:15:000 (1)");
assertMovedScreenTo(EditorScreenMode.Compose);
addStepScreenModeTo(EditorScreenMode.Timing);
addStepClickLink("00:20:000");
assertOnScreenAt(EditorScreenMode.Timing, 20_000);
addStepClickLink("00:25:000 (0,1)");
assertMovedScreenTo(EditorScreenMode.Compose);
addStepScreenModeTo(EditorScreenMode.Verify);
addStepClickLink("00:30:000");
assertOnScreenAt(EditorScreenMode.Verify, 30_000);
addStepClickLink("00:35:000 (0,1)");
assertMovedScreenTo(EditorScreenMode.Compose);
addStepClickLink("00:00:000");
assertOnScreenAt(EditorScreenMode.Compose, 0);
}
private void addStepClickLink(string timestamp, string step = "", bool waitForSeek = true)
{
AddStep($"{step} {timestamp}", () =>
Game.HandleLink(new LinkDetails(LinkAction.OpenEditorTimestamp, timestamp))
);
if (waitForSeek)
AddUntilStep("wait for seek", () => editorClock.SeekingOrStopped.Value);
}
private void addStepScreenModeTo(EditorScreenMode screenMode) =>
AddStep("change screen to " + screenMode, () => editor.Mode.Value = screenMode);
private void assertOnScreenAt(EditorScreenMode screen, double time)
{
AddAssert($"stayed on {screen} at {time}", () =>
editor.Mode.Value == screen
&& editorClock.CurrentTime == time
);
}
private void assertMovedScreenTo(EditorScreenMode screen, string text = "moved to") =>
AddAssert($"{text} {screen}", () => editor.Mode.Value == screen);
private void setUpEditor(RulesetInfo ruleset)
{
BeatmapSetInfo beatmapSet = null!;
AddStep("Import test beatmap", () =>
Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely()
);
AddStep("Retrieve beatmap", () =>
beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach()
);
AddStep("Present beatmap", () => Game.PresentBeatmap(beatmapSet));
AddUntilStep("Wait for song select", () =>
Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
&& songSelect.IsLoaded
);
AddStep("Switch ruleset", () => Game.Ruleset.Value = ruleset);
AddStep("Open editor for ruleset", () =>
((PlaySongSelect)Game.ScreenStack.CurrentScreen)
.Edit(beatmapSet.Beatmaps.Last(beatmap => beatmap.Ruleset.Name == ruleset.Name))
);
AddUntilStep("Wait for editor open", () => editor.ReadyForUse);
}
}
}

View File

@ -114,23 +114,25 @@ namespace osu.Game.Tests.Visual.Gameplay
{
}
[TestCase(PathType.Linear)]
[TestCase(PathType.Bezier)]
[TestCase(PathType.Catmull)]
[TestCase(PathType.PerfectCurve)]
public void TestSingleSegment(PathType type)
=> AddStep("create path", () => path.ControlPoints.AddRange(createSegment(type, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
[TestCase(SplineType.Linear, null)]
[TestCase(SplineType.BSpline, null)]
[TestCase(SplineType.BSpline, 3)]
[TestCase(SplineType.Catmull, null)]
[TestCase(SplineType.PerfectCurve, null)]
public void TestSingleSegment(SplineType splineType, int? degree)
=> AddStep("create path", () => path.ControlPoints.AddRange(createSegment(new PathType { Type = splineType, Degree = degree }, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
[TestCase(PathType.Linear)]
[TestCase(PathType.Bezier)]
[TestCase(PathType.Catmull)]
[TestCase(PathType.PerfectCurve)]
public void TestMultipleSegment(PathType type)
[TestCase(SplineType.Linear, null)]
[TestCase(SplineType.BSpline, null)]
[TestCase(SplineType.BSpline, 3)]
[TestCase(SplineType.Catmull, null)]
[TestCase(SplineType.PerfectCurve, null)]
public void TestMultipleSegment(SplineType splineType, int? degree)
{
AddStep("create path", () =>
{
path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero));
path.ControlPoints.AddRange(createSegment(type, new Vector2(0, 100), new Vector2(100), Vector2.Zero));
path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero));
path.ControlPoints.AddRange(createSegment(new PathType { Type = splineType, Degree = degree }, new Vector2(0, 100), new Vector2(100), Vector2.Zero));
});
}
@ -139,9 +141,9 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddStep("create path", () =>
{
path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(100, 0)));
path.ControlPoints.AddRange(createSegment(PathType.Bezier, new Vector2(100, 0), new Vector2(150, 30), new Vector2(100, 100)));
path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, new Vector2(100, 100), new Vector2(25, 50), Vector2.Zero));
path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(100, 0)));
path.ControlPoints.AddRange(createSegment(PathType.BEZIER, new Vector2(100, 0), new Vector2(150, 30), new Vector2(100, 100)));
path.ControlPoints.AddRange(createSegment(PathType.PERFECT_CURVE, new Vector2(100, 100), new Vector2(25, 50), Vector2.Zero));
});
}
@ -157,7 +159,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddStep("create path", () =>
{
path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(width / 2, height), new Vector2(width, 0)));
path.ControlPoints.AddRange(createSegment(PathType.PERFECT_CURVE, Vector2.Zero, new Vector2(width / 2, height), new Vector2(width, 0)));
});
}
@ -170,11 +172,11 @@ namespace osu.Game.Tests.Visual.Gameplay
switch (points)
{
case 2:
path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(0, 100)));
path.ControlPoints.AddRange(createSegment(PathType.PERFECT_CURVE, Vector2.Zero, new Vector2(0, 100)));
break;
case 4:
path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)));
path.ControlPoints.AddRange(createSegment(PathType.PERFECT_CURVE, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)));
break;
}
});

View File

@ -88,7 +88,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
HitWindows = new HitWindows(),
StartTime = t += spacing,
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }),
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, Vector2.UnitY * 200 }),
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, HitSampleInfo.BANK_SOFT) },
},
});

View File

@ -52,59 +52,68 @@ namespace osu.Game.Tests.Visual.Gameplay
{
}
[TestCase(PathType.Linear)]
[TestCase(PathType.Bezier)]
[TestCase(PathType.Catmull)]
[TestCase(PathType.PerfectCurve)]
public void TestSingleSegment(PathType type)
=> AddStep("create path", () => path.ControlPoints.AddRange(createSegment(type, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
[TestCase(SplineType.Linear, null)]
[TestCase(SplineType.BSpline, null)]
[TestCase(SplineType.BSpline, 3)]
[TestCase(SplineType.Catmull, null)]
[TestCase(SplineType.PerfectCurve, null)]
public void TestSingleSegment(SplineType splineType, int? degree)
=> AddStep("create path", () => path.ControlPoints.AddRange(createSegment(
new PathType { Type = splineType, Degree = degree },
Vector2.Zero,
new Vector2(0, 100),
new Vector2(100),
new Vector2(0, 200),
new Vector2(200)
)));
[TestCase(PathType.Linear)]
[TestCase(PathType.Bezier)]
[TestCase(PathType.Catmull)]
[TestCase(PathType.PerfectCurve)]
public void TestMultipleSegment(PathType type)
[TestCase(SplineType.Linear, null)]
[TestCase(SplineType.BSpline, null)]
[TestCase(SplineType.BSpline, 3)]
[TestCase(SplineType.Catmull, null)]
[TestCase(SplineType.PerfectCurve, null)]
public void TestMultipleSegment(SplineType splineType, int? degree)
{
AddStep("create path", () =>
{
path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero));
path.ControlPoints.AddRange(createSegment(type, new Vector2(0, 100), new Vector2(100), Vector2.Zero));
path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero));
path.ControlPoints.AddRange(createSegment(new PathType { Type = splineType, Degree = degree }, new Vector2(0, 100), new Vector2(100), Vector2.Zero));
});
}
[Test]
public void TestAddControlPoint()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100))));
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100))));
AddStep("add point", () => path.ControlPoints.Add(new PathControlPoint { Position = new Vector2(100) }));
}
[Test]
public void TestInsertControlPoint()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(100))));
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(100))));
AddStep("insert point", () => path.ControlPoints.Insert(1, new PathControlPoint { Position = new Vector2(0, 100) }));
}
[Test]
public void TestRemoveControlPoint()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("remove second point", () => path.ControlPoints.RemoveAt(1));
}
[Test]
public void TestChangePathType()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("change type to bezier", () => path.ControlPoints[0].Type = PathType.Bezier);
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("change type to bezier", () => path.ControlPoints[0].Type = PathType.BEZIER);
}
[Test]
public void TestAddSegmentByChangingType()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0))));
AddStep("change second point type to bezier", () => path.ControlPoints[1].Type = PathType.Bezier);
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0))));
AddStep("change second point type to bezier", () => path.ControlPoints[1].Type = PathType.BEZIER);
}
[Test]
@ -112,8 +121,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddStep("create path", () =>
{
path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)));
path.ControlPoints[1].Type = PathType.Bezier;
path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)));
path.ControlPoints[1].Type = PathType.BEZIER;
});
AddStep("change second point type to null", () => path.ControlPoints[1].Type = null);
@ -124,8 +133,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddStep("create path", () =>
{
path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)));
path.ControlPoints[1].Type = PathType.Bezier;
path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)));
path.ControlPoints[1].Type = PathType.BEZIER;
});
AddStep("remove second point", () => path.ControlPoints.RemoveAt(1));
@ -140,11 +149,11 @@ namespace osu.Game.Tests.Visual.Gameplay
switch (points)
{
case 2:
path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(0, 100)));
path.ControlPoints.AddRange(createSegment(PathType.PERFECT_CURVE, Vector2.Zero, new Vector2(0, 100)));
break;
case 4:
path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)));
path.ControlPoints.AddRange(createSegment(PathType.PERFECT_CURVE, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)));
break;
}
});
@ -153,35 +162,35 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestLengthenLastSegment()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("lengthen last segment", () => path.ExpectedDistance.Value = 300);
}
[Test]
public void TestShortenLastSegment()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("shorten last segment", () => path.ExpectedDistance.Value = 150);
}
[Test]
public void TestShortenFirstSegment()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("shorten first segment", () => path.ExpectedDistance.Value = 50);
}
[Test]
public void TestShortenToZeroLength()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("shorten to 0 length", () => path.ExpectedDistance.Value = 0);
}
[Test]
public void TestShortenToNegativeLength()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("shorten to -10 length", () => path.ExpectedDistance.Value = -10);
}
@ -197,7 +206,7 @@ namespace osu.Game.Tests.Visual.Gameplay
};
double[] distances = { 100d, 200d, 300d };
AddStep("create path", () => path.ControlPoints.AddRange(positions.Select(p => new PathControlPoint(p, PathType.Linear))));
AddStep("create path", () => path.ControlPoints.AddRange(positions.Select(p => new PathControlPoint(p, PathType.LINEAR))));
AddAssert("segment ends are correct", () => path.GetSegmentEnds(), () => Is.EqualTo(distances.Select(d => d / 300)));
AddAssert("segment end positions recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)), () => Is.EqualTo(positions.Skip(1)));

View File

@ -3,6 +3,7 @@
#nullable disable
using System;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@ -56,6 +57,7 @@ namespace osu.Game.Tests.Visual.Gameplay
scoreProcessor.RevertResult(
new JudgementResult(new HitCircle { HitWindows = hitWindows }, new Judgement())
{
GameplayRate = 1.0,
TimeOffset = 25,
Type = HitResult.Perfect,
});
@ -80,6 +82,27 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("UR = 250", () => counter.Current.Value == 250.0);
}
[Test]
public void TestStaticRateChange()
{
AddStep("Create Display", recreateDisplay);
AddRepeatStep("Set UR to 250 at 1.5x", () => applyJudgement(25, true, 1.5), 4);
AddUntilStep("UR = 250/1.5", () => counter.Current.Value == Math.Round(250.0 / 1.5));
}
[Test]
public void TestDynamicRateChange()
{
AddStep("Create Display", recreateDisplay);
AddRepeatStep("Set UR to 100 at 1.0x", () => applyJudgement(10, true, 1.0), 4);
AddRepeatStep("Bring UR to 100 at 1.5x", () => applyJudgement(15, true, 1.5), 4);
AddUntilStep("UR = 100", () => counter.Current.Value == 100.0);
}
private void recreateDisplay()
{
Clear();
@ -92,7 +115,7 @@ namespace osu.Game.Tests.Visual.Gameplay
});
}
private void applyJudgement(double offsetMs, bool alt)
private void applyJudgement(double offsetMs, bool alt, double gameplayRate = 1.0)
{
double placement = offsetMs;
@ -105,6 +128,7 @@ namespace osu.Game.Tests.Visual.Gameplay
scoreProcessor.ApplyResult(new JudgementResult(new HitCircle { HitWindows = hitWindows }, new Judgement())
{
TimeOffset = placement,
GameplayRate = gameplayRate,
Type = HitResult.Perfect,
});
}

View File

@ -78,6 +78,31 @@ namespace osu.Game.Tests.Visual.Online
AddStep("complete request", () => pendingRequest.TriggerSuccess(TEST_USER));
}
[Test]
public void TestLogin()
{
GetUserRequest pendingRequest = null!;
AddStep("set up request handling", () =>
{
dummyAPI.HandleRequest = req =>
{
if (dummyAPI.State.Value == APIState.Online && req is GetUserRequest getUserRequest)
{
pendingRequest = getUserRequest;
return true;
}
return false;
};
});
AddStep("logout", () => dummyAPI.Logout());
AddStep("show user", () => profile.ShowUser(new APIUser { Id = 1 }));
AddStep("login", () => dummyAPI.Login("username", "password"));
AddWaitStep("wait some", 3);
AddStep("complete request", () => pendingRequest.TriggerSuccess(TEST_USER));
}
public static readonly APIUser TEST_USER = new APIUser
{
Username = @"Somebody",

View File

@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestAroundCentre()
{
createTest(Enumerable.Range(-150, 300).Select(i => new HitEvent(i / 50f, HitResult.Perfect, placeholder_object, placeholder_object, null)).ToList());
createTest(Enumerable.Range(-150, 300).Select(i => new HitEvent(i / 50f, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null)).ToList());
}
[Test]
@ -57,12 +57,12 @@ namespace osu.Game.Tests.Visual.Ranking
{
createTest(new List<HitEvent>
{
new HitEvent(-7, HitResult.Perfect, placeholder_object, placeholder_object, null),
new HitEvent(-6, HitResult.Perfect, placeholder_object, placeholder_object, null),
new HitEvent(-5, HitResult.Perfect, placeholder_object, placeholder_object, null),
new HitEvent(5, HitResult.Perfect, placeholder_object, placeholder_object, null),
new HitEvent(6, HitResult.Perfect, placeholder_object, placeholder_object, null),
new HitEvent(7, HitResult.Perfect, placeholder_object, placeholder_object, null),
new HitEvent(-7, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null),
new HitEvent(-6, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null),
new HitEvent(-5, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null),
new HitEvent(5, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null),
new HitEvent(6, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null),
new HitEvent(7, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null),
});
}
@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual.Ranking
: offset > 16 ? HitResult.Good
: offset > 8 ? HitResult.Great
: HitResult.Perfect;
return new HitEvent(h.TimeOffset, result, placeholder_object, placeholder_object, null);
return new HitEvent(h.TimeOffset, 1.0, result, placeholder_object, placeholder_object, null);
}).ToList());
}
@ -95,7 +95,7 @@ namespace osu.Game.Tests.Visual.Ranking
: offset > 8 ? HitResult.Great
: HitResult.Perfect;
return new HitEvent(h.TimeOffset, result, placeholder_object, placeholder_object, null);
return new HitEvent(h.TimeOffset, 1.0, result, placeholder_object, placeholder_object, null);
});
var narrow = CreateDistributedHitEvents(0, 50).Select(h =>
{
@ -106,7 +106,7 @@ namespace osu.Game.Tests.Visual.Ranking
: offset > 10 ? HitResult.Good
: offset > 5 ? HitResult.Great
: HitResult.Perfect;
return new HitEvent(h.TimeOffset, result, placeholder_object, placeholder_object, null);
return new HitEvent(h.TimeOffset, 1.0, result, placeholder_object, placeholder_object, null);
});
createTest(wide.Concat(narrow).ToList());
}
@ -114,7 +114,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestZeroTimeOffset()
{
createTest(Enumerable.Range(0, 100).Select(_ => new HitEvent(0, HitResult.Perfect, placeholder_object, placeholder_object, null)).ToList());
createTest(Enumerable.Range(0, 100).Select(_ => new HitEvent(0, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null)).ToList());
}
[Test]
@ -129,9 +129,9 @@ namespace osu.Game.Tests.Visual.Ranking
createTest(Enumerable.Range(0, 100).Select(i =>
{
if (i % 2 == 0)
return new HitEvent(0, HitResult.Perfect, placeholder_object, placeholder_object, null);
return new HitEvent(0, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null);
return new HitEvent(30, HitResult.Miss, placeholder_object, placeholder_object, null);
return new HitEvent(30, 1.0, HitResult.Miss, placeholder_object, placeholder_object, null);
}).ToList());
}
@ -162,7 +162,7 @@ namespace osu.Game.Tests.Visual.Ranking
int count = (int)(Math.Pow(range - Math.Abs(i - range), 2)) / 10;
for (int j = 0; j < count; j++)
hitEvents.Add(new HitEvent(centre + i - range, HitResult.Perfect, placeholder_object, placeholder_object, null));
hitEvents.Add(new HitEvent(centre + i - range, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null));
}
return hitEvents;

View File

@ -437,7 +437,7 @@ namespace osu.Game.Beatmaps.Formats
// Explicit segments have a new format in which the type is injected into the middle of the control point string.
// To preserve compatibility with osu-stable as much as possible, explicit segments with the same type are converted to use implicit segments by duplicating the control point.
// One exception are consecutive perfect curves, which aren't supported in osu!stable and can lead to decoding issues if encoded as implicit segments
bool needsExplicitSegment = point.Type != lastType || point.Type == PathType.PerfectCurve;
bool needsExplicitSegment = point.Type != lastType || point.Type == PathType.PERFECT_CURVE;
// Another exception to this is when the last two control points of the last segment were duplicated. This is not a scenario supported by osu!stable.
// Lazer does not add implicit segments for the last two control points of _any_ explicit segment, so an explicit segment is forced in order to maintain consistency with the decoder.
@ -453,21 +453,21 @@ namespace osu.Game.Beatmaps.Formats
if (needsExplicitSegment)
{
switch (point.Type)
switch (point.Type?.Type)
{
case PathType.Bezier:
writer.Write("B|");
case SplineType.BSpline:
writer.Write(point.Type.Value.Degree > 0 ? $"B{point.Type.Value.Degree}|" : "B|");
break;
case PathType.Catmull:
case SplineType.Catmull:
writer.Write("C|");
break;
case PathType.PerfectCurve:
case SplineType.PerfectCurve:
writer.Write("P|");
break;
case PathType.Linear:
case SplineType.Linear:
writer.Write("L|");
break;
}

View File

@ -78,14 +78,15 @@ namespace osu.Game.Database
// wherein the last control point of an otherwise-single-segment slider path has a different type than previous,
// which would lead to sliders being mangled when exported back to stable.
// normally, that would be handled by the `BezierConverter.ConvertToModernBezier()` call below,
// which outputs a slider path containing only Bezier control points,
// which outputs a slider path containing only BEZIER control points,
// but a non-inherited last control point is (rightly) not considered to be starting a new segment,
// therefore it would fail to clear the `CountSegments() <= 1` check.
// by clearing explicitly we both fix the issue and avoid unnecessary conversions to Bezier.
// by clearing explicitly we both fix the issue and avoid unnecessary conversions to BEZIER.
if (hasPath.Path.ControlPoints.Count > 1)
hasPath.Path.ControlPoints[^1].Type = null;
if (BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1) continue;
if (BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1
&& hasPath.Path.ControlPoints[0].Type!.Value.Degree == null) continue;
var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints);

View File

@ -6,13 +6,15 @@
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osuTK.Graphics;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterface
{
@ -78,6 +80,9 @@ namespace osu.Game.Graphics.UserInterface
{
case StatefulMenuItem stateful:
return new DrawableStatefulMenuItem(stateful);
case OsuMenuItemSpacer spacer:
return new DrawableSpacer(spacer);
}
return new DrawableOsuMenuItem(item);
@ -89,5 +94,28 @@ namespace osu.Game.Graphics.UserInterface
{
Anchor = Direction == Direction.Horizontal ? Anchor.BottomLeft : Anchor.TopRight
};
protected partial class DrawableSpacer : DrawableOsuMenuItem
{
public DrawableSpacer(MenuItem item)
: base(item)
{
Scale = new Vector2(1, 0.6f);
AddInternal(new Box
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = BackgroundColourHover,
RelativeSizeAxes = Axes.X,
Height = 2f,
Width = 0.8f,
});
}
protected override bool OnHover(HoverEvent e) => true;
protected override bool OnClick(ClickEvent e) => true;
}
}
}

View File

@ -0,0 +1,13 @@
// 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.
namespace osu.Game.Graphics.UserInterface
{
public class OsuMenuItemSpacer : OsuMenuItem
{
public OsuMenuItemSpacer()
: base(" ")
{
}
}
}

View File

@ -119,6 +119,16 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString LimitedDistanceSnap => new TranslatableString(getKey(@"limited_distance_snap_grid"), @"Limit distance snap placement to current time");
/// <summary>
/// "Must be in edit mode to handle editor links"
/// </summary>
public static LocalisableString MustBeInEditorToHandleLinks => new TranslatableString(getKey(@"must_be_in_editor_to_handle_links"), @"Must be in edit mode to handle editor links");
/// <summary>
/// "Failed to parse editor link"
/// </summary>
public static LocalisableString FailedToParseEditorLink => new TranslatableString(getKey(@"failed_to_parse_edtior_link"), @"Failed to parse editor link");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Edit;
namespace osu.Game.Online.Chat
{
@ -41,10 +42,6 @@ namespace osu.Game.Online.Chat
@"(?:#(?:[a-z0-9$_\+!\*\',;:\(\)@&=\/~-]|%[0-9a-f]{2})*)?)?)",
RegexOptions.IgnoreCase);
// 00:00:000 (1,2,3) - test
// regex from https://github.com/ppy/osu-web/blob/651a9bac2b60d031edd7e33b8073a469bf11edaa/resources/assets/coffee/_classes/beatmap-discussion-helper.coffee#L10
private static readonly Regex time_regex = new Regex(@"\b(((\d{2,}):([0-5]\d)[:.](\d{3}))(\s\((?:\d+[,|])*\d+\))?)");
// #osu
private static readonly Regex channel_regex = new Regex(@"(#[a-zA-Z]+[a-zA-Z0-9]+)");
@ -274,7 +271,7 @@ namespace osu.Game.Online.Chat
handleAdvanced(advanced_link_regex, result, startIndex);
// handle editor times
handleMatches(time_regex, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}edit/{{0}}", result, startIndex, LinkAction.OpenEditorTimestamp);
handleMatches(EditorTimestampParser.TIME_REGEX, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}edit/{{0}}", result, startIndex, LinkAction.OpenEditorTimestamp);
// handle channels
handleMatches(channel_regex, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}chan/{{0}}", result, startIndex, LinkAction.OpenChannel);

View File

@ -58,6 +58,7 @@ using osu.Game.Performance;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Menu;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.Play;
@ -433,6 +434,9 @@ namespace osu.Game
break;
case LinkAction.OpenEditorTimestamp:
HandleTimestamp(argString);
break;
case LinkAction.JoinMultiplayerMatch:
case LinkAction.Spectate:
waitForReady(() => Notifications, _ => Notifications.Post(new SimpleNotification
@ -550,6 +554,25 @@ namespace osu.Game
/// <param name="version">The build version of the update stream</param>
public void ShowChangelogBuild(string updateStream, string version) => waitForReady(() => changelogOverlay, _ => changelogOverlay.ShowBuild(updateStream, version));
/// <summary>
/// Seeks to the provided <paramref name="timestamp"/> if the editor is currently open.
/// Can also select objects as indicated by the <paramref name="timestamp"/> (depends on ruleset implementation).
/// </summary>
public void HandleTimestamp(string timestamp)
{
if (ScreenStack.CurrentScreen is not Editor editor)
{
Schedule(() => Notifications.Post(new SimpleErrorNotification
{
Icon = FontAwesome.Solid.ExclamationTriangle,
Text = EditorStrings.MustBeInEditorToHandleLinks
}));
return;
}
editor.HandleTimestamp(timestamp);
}
/// <summary>
/// Present a skin select immediately.
/// </summary>

View File

@ -151,23 +151,23 @@ namespace osu.Game.Overlays.SkinEditor
{
new MenuItem(CommonStrings.MenuBarFile)
{
Items = new[]
Items = new OsuMenuItem[]
{
new EditorMenuItem(Web.CommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()),
new EditorMenuItem(CommonStrings.Export, MenuItemType.Standard, () => skins.ExportCurrentSkin()) { Action = { Disabled = !RuntimeInfo.IsDesktop } },
new EditorMenuItemSpacer(),
new OsuMenuItemSpacer(),
new EditorMenuItem(CommonStrings.RevertToDefault, MenuItemType.Destructive, () => dialogOverlay?.Push(new RevertConfirmDialog(revert))),
new EditorMenuItemSpacer(),
new OsuMenuItemSpacer(),
new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, () => skinEditorOverlay?.Hide()),
},
},
new MenuItem(CommonStrings.MenuBarEdit)
{
Items = new[]
Items = new OsuMenuItem[]
{
undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo),
redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo),
new EditorMenuItemSpacer(),
new OsuMenuItemSpacer(),
cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut),
copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy),
pasteMenuItem = new EditorMenuItem(CommonStrings.Paste, MenuItemType.Standard, Paste),

View File

@ -188,7 +188,10 @@ namespace osu.Game.Overlays.SkinEditor
}
if (skinEditor.State.Value == Visibility.Visible)
{
skinEditor.Save(false);
skinEditor.UpdateTargetScreen(target);
}
else
{
skinEditor.Hide();

View File

@ -14,7 +14,6 @@ using osu.Framework.Utils;
using osu.Game.Extensions;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Components.Menus;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Skinning;
using osu.Game.Utils;
@ -249,7 +248,7 @@ namespace osu.Game.Overlays.SkinEditor
Items = createAnchorItems((d, o) => ((Drawable)d).Origin == o, applyOrigins).ToArray()
};
yield return new EditorMenuItemSpacer();
yield return new OsuMenuItemSpacer();
yield return new OsuMenuItem("Reset position", MenuItemType.Standard, () =>
{
@ -277,13 +276,13 @@ namespace osu.Game.Overlays.SkinEditor
}
});
yield return new EditorMenuItemSpacer();
yield return new OsuMenuItemSpacer();
yield return new OsuMenuItem("Bring to front", MenuItemType.Standard, () => skinEditor.BringSelectionToFront());
yield return new OsuMenuItem("Send to back", MenuItemType.Standard, () => skinEditor.SendSelectionToBack());
yield return new EditorMenuItemSpacer();
yield return new OsuMenuItemSpacer();
foreach (var item in base.GetContextMenuItemsForSelection(selection))
yield return item;

View File

@ -5,6 +5,7 @@ using System;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -18,6 +19,7 @@ using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Profile;
@ -42,6 +44,11 @@ namespace osu.Game.Overlays
private ProfileSectionsContainer? sectionsContainer;
private ProfileSectionTabControl? tabs;
private IUser? user;
private IRulesetInfo? ruleset;
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
@ -58,16 +65,36 @@ namespace osu.Game.Overlays
});
}
[BackgroundDependencyLoader]
private void load()
{
apiState.BindTo(API.State);
apiState.BindValueChanged(state => Schedule(() =>
{
if (state.NewValue == APIState.Online && user != null)
Scheduler.AddOnce(fetchAndSetContent);
}));
}
protected override ProfileHeader CreateHeader() => new ProfileHeader();
protected override Color4 BackgroundColour => ColourProvider.Background5;
public void ShowUser(IUser user, IRulesetInfo? ruleset = null)
public void ShowUser(IUser userToShow, IRulesetInfo? userRuleset = null)
{
if (user.OnlineID == APIUser.SYSTEM_USER_ID)
if (userToShow.OnlineID == APIUser.SYSTEM_USER_ID)
return;
user = userToShow;
ruleset = userRuleset;
Show();
Scheduler.AddOnce(fetchAndSetContent);
}
private void fetchAndSetContent()
{
Debug.Assert(user != null);
if (user.OnlineID == Header.User.Value?.User.Id && ruleset?.MatchesOnlineID(Header.User.Value?.Ruleset) == true)
return;
@ -143,24 +170,28 @@ namespace osu.Game.Overlays
sectionsContainer.ScrollToTop();
userReq = user.OnlineID > 1 ? new GetUserRequest(user.OnlineID, ruleset) : new GetUserRequest(user.Username, ruleset);
userReq.Success += u => userLoadComplete(u, ruleset);
API.Queue(userReq);
loadingLayer.Show();
if (API.State.Value != APIState.Offline)
{
userReq = user.OnlineID > 1 ? new GetUserRequest(user.OnlineID, ruleset) : new GetUserRequest(user.Username, ruleset);
userReq.Success += u => userLoadComplete(u, ruleset);
API.Queue(userReq);
loadingLayer.Show();
}
}
private void userLoadComplete(APIUser user, IRulesetInfo? ruleset)
private void userLoadComplete(APIUser loadedUser, IRulesetInfo? userRuleset)
{
Debug.Assert(sections != null && sectionsContainer != null && tabs != null);
var actualRuleset = rulesets.GetRuleset(ruleset?.ShortName ?? user.PlayMode).AsNonNull();
var actualRuleset = rulesets.GetRuleset(userRuleset?.ShortName ?? loadedUser.PlayMode).AsNonNull();
var userProfile = new UserProfileData(user, actualRuleset);
var userProfile = new UserProfileData(loadedUser, actualRuleset);
Header.User.Value = userProfile;
if (user.ProfileOrder != null)
if (loadedUser.ProfileOrder != null)
{
foreach (string id in user.ProfileOrder)
foreach (string id in loadedUser.ProfileOrder)
{
var sec = sections.FirstOrDefault(s => s.Identifier == id);

View File

@ -0,0 +1,50 @@
// 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 System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
namespace osu.Game.Rulesets.Edit
{
public static class EditorTimestampParser
{
// 00:00:000 (...) - test
// original osu-web regex: https://github.com/ppy/osu-web/blob/3b1698639244cfdaf0b41c68bfd651ea729ec2e3/resources/js/utils/beatmapset-discussion-helper.ts#L78
public static readonly Regex TIME_REGEX = new Regex(@"\b(((?<minutes>\d{2,}):(?<seconds>[0-5]\d)[:.](?<milliseconds>\d{3}))(?<selection>\s\([^)]+\))?)", RegexOptions.Compiled);
public static bool TryParse(string timestamp, [NotNullWhen(true)] out TimeSpan? parsedTime, out string? parsedSelection)
{
Match match = TIME_REGEX.Match(timestamp);
if (!match.Success)
{
parsedTime = null;
parsedSelection = null;
return false;
}
bool result = true;
result &= int.TryParse(match.Groups[@"minutes"].Value, out int timeMin);
result &= int.TryParse(match.Groups[@"seconds"].Value, out int timeSec);
result &= int.TryParse(match.Groups[@"milliseconds"].Value, out int timeMsec);
// somewhat sane limit for timestamp duration (10 hours).
result &= timeMin < 600;
if (!result)
{
parsedTime = null;
parsedSelection = null;
return false;
}
parsedTime = TimeSpan.FromMinutes(timeMin) + TimeSpan.FromSeconds(timeSec) + TimeSpan.FromMilliseconds(timeMsec);
parsedSelection = match.Groups[@"selection"].Value.Trim();
if (!string.IsNullOrEmpty(parsedSelection))
parsedSelection = parsedSelection[1..^1];
return true;
}
}
}

View File

@ -526,8 +526,20 @@ namespace osu.Game.Rulesets.Edit
/// </summary>
public abstract bool CursorInPlacementArea { get; }
/// <summary>
/// Returns a string representing the current selection.
/// The inverse method to <see cref="SelectFromTimestamp"/>.
/// </summary>
public virtual string ConvertSelectionToString() => string.Empty;
/// <summary>
/// Selects objects based on the supplied <paramref name="timestamp"/> and <paramref name="objectDescription"/>.
/// The inverse method to <see cref="ConvertSelectionToString"/>.
/// </summary>
/// <param name="timestamp">The time instant to seek to, in milliseconds.</param>
/// <param name="objectDescription">The ruleset-specific description of objects to select at the given timestamp.</param>
public virtual void SelectFromTimestamp(double timestamp, string objectDescription) { }
#region IPositionSnapProvider
public abstract SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All);

View File

@ -54,6 +54,11 @@ namespace osu.Game.Rulesets.Judgements
/// </remarks>
public double TimeAbsolute => RawTime != null ? Math.Min(RawTime.Value, HitObject.GetEndTime() + HitObject.MaximumJudgementOffset) : HitObject.GetEndTime();
/// <summary>
/// The gameplay rate at the time this <see cref="JudgementResult"/> occurred.
/// </summary>
public double? GameplayRate { get; internal set; }
/// <summary>
/// The combo prior to this <see cref="JudgementResult"/> occurring.
/// </summary>

View File

@ -68,32 +68,35 @@ namespace osu.Game.Rulesets.Objects
// The current vertex ends the segment
var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1);
var segmentType = controlPoints[start].Type ?? PathType.Linear;
var segmentType = controlPoints[start].Type ?? PathType.LINEAR;
switch (segmentType)
switch (segmentType.Type)
{
case PathType.Catmull:
case SplineType.Catmull:
result.AddRange(from segment in ConvertCatmullToBezierAnchors(segmentVertices) from v in segment select v + position);
break;
case PathType.Linear:
case SplineType.Linear:
result.AddRange(from segment in ConvertLinearToBezierAnchors(segmentVertices) from v in segment select v + position);
break;
case PathType.PerfectCurve:
case SplineType.PerfectCurve:
result.AddRange(ConvertCircleToBezierAnchors(segmentVertices).Select(v => v + position));
break;
default:
case SplineType.BSpline:
if (segmentType.Degree != null)
throw new NotImplementedException("BSpline conversion of arbitrary degree is not implemented.");
foreach (Vector2 v in segmentVertices)
{
result.Add(v + position);
}
break;
default:
throw new ArgumentOutOfRangeException(nameof(segmentType.Type), segmentType.Type, "Unsupported segment type found when converting to legacy Bezier");
}
// Start the new segment at the current vertex
@ -104,7 +107,7 @@ namespace osu.Game.Rulesets.Objects
}
/// <summary>
/// Converts a path of control points to an identical path using only Bezier type control points.
/// Converts a path of control points to an identical path using only BEZIER type control points.
/// </summary>
/// <param name="controlPoints">The control points of the path.</param>
/// <returns>The list of bezier control points.</returns>
@ -124,49 +127,56 @@ namespace osu.Game.Rulesets.Objects
// The current vertex ends the segment
var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1);
var segmentType = controlPoints[start].Type ?? PathType.Linear;
var segmentType = controlPoints[start].Type ?? PathType.LINEAR;
switch (segmentType)
switch (segmentType.Type)
{
case PathType.Catmull:
case SplineType.Catmull:
foreach (var segment in ConvertCatmullToBezierAnchors(segmentVertices))
{
for (int j = 0; j < segment.Length - 1; j++)
{
result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.Bezier : null));
result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.BEZIER : null));
}
}
break;
case PathType.Linear:
case SplineType.Linear:
foreach (var segment in ConvertLinearToBezierAnchors(segmentVertices))
{
for (int j = 0; j < segment.Length - 1; j++)
{
result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.Bezier : null));
result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.BEZIER : null));
}
}
break;
case PathType.PerfectCurve:
case SplineType.PerfectCurve:
var circleResult = ConvertCircleToBezierAnchors(segmentVertices);
for (int j = 0; j < circleResult.Length - 1; j++)
{
result.Add(new PathControlPoint(circleResult[j], j == 0 ? PathType.Bezier : null));
result.Add(new PathControlPoint(circleResult[j], j == 0 ? PathType.BEZIER : null));
}
break;
case SplineType.BSpline:
var bSplineResult = segmentType.Degree == null
? segmentVertices
: PathApproximator.BSplineToBezier(segmentVertices, segmentType.Degree.Value);
for (int j = 0; j < bSplineResult.Length - 1; j++)
{
result.Add(new PathControlPoint(bSplineResult[j], j == 0 ? PathType.BEZIER : null));
}
break;
default:
for (int j = 0; j < segmentVertices.Length - 1; j++)
{
result.Add(new PathControlPoint(segmentVertices[j], j == 0 ? PathType.Bezier : null));
}
break;
throw new ArgumentOutOfRangeException(nameof(segmentType.Type), segmentType.Type, "Unsupported segment type found when converting to legacy Bezier");
}
// Start the new segment at the current vertex

View File

@ -704,6 +704,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
}
Result.RawTime = Time.Current;
Result.GameplayRate = (Clock as IGameplayClock)?.GetTrueGameplayRate() ?? Clock.Rate;
if (Result.HasResult)
updateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss);

View File

@ -224,16 +224,19 @@ namespace osu.Game.Rulesets.Objects.Legacy
{
default:
case 'C':
return PathType.Catmull;
return PathType.CATMULL;
case 'B':
return PathType.Bezier;
if (input.Length > 1 && int.TryParse(input.Substring(1), out int degree) && degree > 0)
return PathType.BSpline(degree);
return PathType.BEZIER;
case 'L':
return PathType.Linear;
return PathType.LINEAR;
case 'P':
return PathType.PerfectCurve;
return PathType.PERFECT_CURVE;
}
}
@ -270,8 +273,8 @@ namespace osu.Game.Rulesets.Objects.Legacy
while (++endIndex < pointSplit.Length)
{
// Keep incrementing endIndex while it's not the start of a new segment (indicated by having a type descriptor of length 1).
if (pointSplit[endIndex].Length > 1)
// Keep incrementing endIndex while it's not the start of a new segment (indicated by having an alpha character at position 0).
if (!char.IsLetter(pointSplit[endIndex][0]))
continue;
// Multi-segmented sliders DON'T contain the end point as part of the current segment as it's assumed to be the start of the next segment.
@ -320,14 +323,14 @@ namespace osu.Game.Rulesets.Objects.Legacy
readPoint(endPoint, offset, out vertices[^1]);
// Edge-case rules (to match stable).
if (type == PathType.PerfectCurve)
if (type == PathType.PERFECT_CURVE)
{
if (vertices.Length != 3)
type = PathType.Bezier;
type = PathType.BEZIER;
else if (isLinear(vertices))
{
// osu-stable special-cased colinear perfect curves to a linear path
type = PathType.Linear;
type = PathType.LINEAR;
}
}
@ -349,10 +352,10 @@ namespace osu.Game.Rulesets.Objects.Legacy
if (vertices[endIndex].Position != vertices[endIndex - 1].Position)
continue;
// Legacy Catmull sliders don't support multiple segments, so adjacent Catmull segments should be treated as a single one.
// Legacy CATMULL sliders don't support multiple segments, so adjacent CATMULL segments should be treated as a single one.
// Importantly, this is not applied to the first control point, which may duplicate the slider path's position
// resulting in a duplicate (0,0) control point in the resultant list.
if (type == PathType.Catmull && endIndex > 1 && FormatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION)
if (type == PathType.CATMULL && endIndex > 1 && FormatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION)
continue;
// The last control point of each segment is not allowed to start a new implicit segment.

View File

@ -260,7 +260,7 @@ namespace osu.Game.Rulesets.Objects
// The current vertex ends the segment
var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1);
var segmentType = ControlPoints[start].Type ?? PathType.Linear;
var segmentType = ControlPoints[start].Type ?? PathType.LINEAR;
// No need to calculate path when there is only 1 vertex
if (segmentVertices.Length == 1)
@ -288,16 +288,16 @@ namespace osu.Game.Rulesets.Objects
private List<Vector2> calculateSubPath(ReadOnlySpan<Vector2> subControlPoints, PathType type)
{
switch (type)
switch (type.Type)
{
case PathType.Linear:
return PathApproximator.ApproximateLinear(subControlPoints);
case SplineType.Linear:
return PathApproximator.LinearToPiecewiseLinear(subControlPoints);
case PathType.PerfectCurve:
case SplineType.PerfectCurve:
if (subControlPoints.Length != 3)
break;
List<Vector2> subPath = PathApproximator.ApproximateCircularArc(subControlPoints);
List<Vector2> subPath = PathApproximator.CircularArcToPiecewiseLinear(subControlPoints);
// If for some reason a circular arc could not be fit to the 3 given points, fall back to a numerically stable bezier approximation.
if (subPath.Count == 0)
@ -305,11 +305,11 @@ namespace osu.Game.Rulesets.Objects
return subPath;
case PathType.Catmull:
return PathApproximator.ApproximateCatmull(subControlPoints);
case SplineType.Catmull:
return PathApproximator.CatmullToPiecewiseLinear(subControlPoints);
}
return PathApproximator.ApproximateBezier(subControlPoints);
return PathApproximator.BSplineToPiecewiseLinear(subControlPoints, type.Degree ?? subControlPoints.Length);
}
private void calculateLength()

View File

@ -29,11 +29,11 @@ namespace osu.Game.Rulesets.Objects
{
var controlPoints = sliderPath.ControlPoints;
var inheritedLinearPoints = controlPoints.Where(p => sliderPath.PointsInSegment(p)[0].Type == PathType.Linear && p.Type is null).ToList();
var inheritedLinearPoints = controlPoints.Where(p => sliderPath.PointsInSegment(p)[0].Type == PathType.LINEAR && p.Type is null).ToList();
// Inherited points after a linear point, as well as the first control point if it inherited,
// should be treated as linear points, so their types are temporarily changed to linear.
inheritedLinearPoints.ForEach(p => p.Type = PathType.Linear);
inheritedLinearPoints.ForEach(p => p.Type = PathType.LINEAR);
double[] segmentEnds = sliderPath.GetSegmentEnds().ToArray();
@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Objects
inheritedLinearPoints.ForEach(p => p.Type = null);
// Recalculate middle perfect curve control points at the end of the slider path.
if (controlPoints.Count >= 3 && controlPoints[^3].Type == PathType.PerfectCurve && controlPoints[^2].Type is null && segmentEnds.Any())
if (controlPoints.Count >= 3 && controlPoints[^3].Type == PathType.PERFECT_CURVE && controlPoints[^2].Type is null && segmentEnds.Any())
{
double lastSegmentStart = segmentEnds.Length > 1 ? segmentEnds[^2] : 0;
double lastSegmentEnd = segmentEnds[^1];

View File

@ -16,6 +16,12 @@ namespace osu.Game.Rulesets.Objects.Types
/// <summary>
/// When starting a new combo, the offset of the new combo relative to the current one.
/// </summary>
/// <remarks>
/// This is generally a setting provided by a beatmap creator to choreograph interesting colour patterns
/// which can only be achieved by skipping combo colours with per-hitobject level.
///
/// It is exposed via <see cref="IHasComboInformation.ComboIndexWithOffsets"/>.
/// </remarks>
int ComboOffset { get; }
}
}

View File

@ -12,6 +12,9 @@ namespace osu.Game.Rulesets.Objects.Types
/// </summary>
public interface IHasComboInformation : IHasCombo
{
/// <summary>
/// Bindable exposure of <see cref="IndexInCurrentCombo"/>.
/// </summary>
Bindable<int> IndexInCurrentComboBindable { get; }
/// <summary>
@ -19,13 +22,21 @@ namespace osu.Game.Rulesets.Objects.Types
/// </summary>
int IndexInCurrentCombo { get; set; }
/// <summary>
/// Bindable exposure of <see cref="ComboIndex"/>.
/// </summary>
Bindable<int> ComboIndexBindable { get; }
/// <summary>
/// The index of this combo in relation to the beatmap.
///
/// In other words, this is incremented by 1 each time a <see cref="NewCombo"/> is reached.
/// </summary>
int ComboIndex { get; set; }
/// <summary>
/// Bindable exposure of <see cref="ComboIndexWithOffsets"/>.
/// </summary>
Bindable<int> ComboIndexWithOffsetsBindable { get; }
/// <summary>
@ -39,6 +50,9 @@ namespace osu.Game.Rulesets.Objects.Types
/// </summary>
new bool NewCombo { get; set; }
/// <summary>
/// Bindable exposure of <see cref="LastInCombo"/>.
/// </summary>
Bindable<bool> LastInComboBindable { get; }
/// <summary>

View File

@ -1,13 +1,87 @@
// 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.Bindables;
namespace osu.Game.Rulesets.Objects.Types
{
public enum PathType
public enum SplineType
{
Catmull,
Bezier,
BSpline,
Linear,
PerfectCurve
}
public readonly struct PathType : IEquatable<PathType>, IHasDescription
{
/// <summary>
/// The type of the spline that should be used to interpret the control points of the path.
/// </summary>
public SplineType Type { get; init; }
/// <summary>
/// The degree of a BSpline. Unused if <see cref="Type"/> is not <see cref="SplineType.BSpline"/>.
/// Null means the degree is equal to the number of control points, 1 means linear, 2 means quadratic, etc.
/// </summary>
public int? Degree { get; init; }
public PathType(SplineType splineType)
{
Type = splineType;
Degree = null;
}
public static readonly PathType CATMULL = new PathType(SplineType.Catmull);
public static readonly PathType BEZIER = new PathType(SplineType.BSpline);
public static readonly PathType LINEAR = new PathType(SplineType.Linear);
public static readonly PathType PERFECT_CURVE = new PathType(SplineType.PerfectCurve);
public static PathType BSpline(int degree)
{
if (degree <= 0)
throw new ArgumentOutOfRangeException(nameof(degree), "The degree of a B-Spline path must be greater than zero.");
return new PathType { Type = SplineType.BSpline, Degree = degree };
}
public string Description
{
get
{
switch (Type)
{
case SplineType.Catmull:
return "Catmull";
case SplineType.BSpline:
return Degree == null ? "Bezier" : "B-spline";
case SplineType.Linear:
return "Linear";
case SplineType.PerfectCurve:
return "Perfect curve";
default:
return Type.ToString();
}
}
}
public override int GetHashCode()
=> HashCode.Combine(Type, Degree);
public override bool Equals(object? obj)
=> obj is PathType pathType && Equals(pathType);
public bool Equals(PathType other)
=> Type == other.Type && Degree == other.Degree;
public static bool operator ==(PathType a, PathType b) => a.Equals(b);
public static bool operator !=(PathType a, PathType b) => !a.Equals(b);
public override string ToString() => Description;
}
}

View File

@ -41,16 +41,29 @@ namespace osu.Game.Rulesets.Scoring
/// </summary>
private const double max_health_target = 0.4;
private IBeatmap beatmap;
/// <summary>
/// The drain rate as a proportion of the total health drained per millisecond.
/// </summary>
public double DrainRate { get; private set; }
private double gameplayEndTime;
/// <summary>
/// The beatmap.
/// </summary>
protected IBeatmap Beatmap { get; private set; }
private readonly double drainStartTime;
private readonly double drainLenience;
/// <summary>
/// The time at which health starts draining.
/// </summary>
protected readonly double DrainStartTime;
/// <summary>
/// An amount of lenience to apply to the drain rate.
/// </summary>
protected readonly double DrainLenience;
private readonly List<(double time, double health)> healthIncreases = new List<(double, double)>();
private double gameplayEndTime;
private double targetMinimumHealth;
private double drainRate = 1;
private PeriodTracker noDrainPeriodTracker;
@ -64,8 +77,8 @@ namespace osu.Game.Rulesets.Scoring
/// A value of 1 completely removes drain.</param>
public DrainingHealthProcessor(double drainStartTime, double drainLenience = 0)
{
this.drainStartTime = drainStartTime;
this.drainLenience = Math.Clamp(drainLenience, 0, 1);
DrainStartTime = drainStartTime;
DrainLenience = Math.Clamp(drainLenience, 0, 1);
}
protected override void Update()
@ -76,16 +89,16 @@ namespace osu.Game.Rulesets.Scoring
return;
// When jumping in and out of gameplay time within a single frame, health should only be drained for the period within the gameplay time
double lastGameplayTime = Math.Clamp(Time.Current - Time.Elapsed, drainStartTime, gameplayEndTime);
double currentGameplayTime = Math.Clamp(Time.Current, drainStartTime, gameplayEndTime);
double lastGameplayTime = Math.Clamp(Time.Current - Time.Elapsed, DrainStartTime, gameplayEndTime);
double currentGameplayTime = Math.Clamp(Time.Current, DrainStartTime, gameplayEndTime);
if (drainLenience < 1)
Health.Value -= drainRate * (currentGameplayTime - lastGameplayTime);
if (DrainLenience < 1)
Health.Value -= DrainRate * (currentGameplayTime - lastGameplayTime);
}
public override void ApplyBeatmap(IBeatmap beatmap)
{
this.beatmap = beatmap;
Beatmap = beatmap;
if (beatmap.HitObjects.Count > 0)
gameplayEndTime = beatmap.HitObjects[^1].GetEndTime();
@ -106,7 +119,7 @@ namespace osu.Game.Rulesets.Scoring
targetMinimumHealth = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, min_health_target, mid_health_target, max_health_target);
// Add back a portion of the amount of HP to be drained, depending on the lenience requested.
targetMinimumHealth += drainLenience * (1 - targetMinimumHealth);
targetMinimumHealth += DrainLenience * (1 - targetMinimumHealth);
// Ensure the target HP is within an acceptable range.
targetMinimumHealth = Math.Clamp(targetMinimumHealth, 0, 1);
@ -126,15 +139,13 @@ namespace osu.Game.Rulesets.Scoring
{
base.Reset(storeResults);
drainRate = 1;
if (storeResults)
drainRate = computeDrainRate();
DrainRate = ComputeDrainRate();
healthIncreases.Clear();
}
private double computeDrainRate()
protected virtual double ComputeDrainRate()
{
if (healthIncreases.Count <= 1)
return 0;
@ -153,17 +164,17 @@ namespace osu.Game.Rulesets.Scoring
for (int i = 0; i < healthIncreases.Count; i++)
{
double currentTime = healthIncreases[i].time;
double lastTime = i > 0 ? healthIncreases[i - 1].time : drainStartTime;
double lastTime = i > 0 ? healthIncreases[i - 1].time : DrainStartTime;
// Subtract any break time from the duration since the last object
if (beatmap.Breaks.Count > 0)
if (Beatmap.Breaks.Count > 0)
{
// Advance the last break occuring before the current time
while (currentBreak + 1 < beatmap.Breaks.Count && beatmap.Breaks[currentBreak + 1].EndTime < currentTime)
while (currentBreak + 1 < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak + 1].EndTime < currentTime)
currentBreak++;
if (currentBreak >= 0)
lastTime = Math.Max(lastTime, beatmap.Breaks[currentBreak].EndTime);
lastTime = Math.Max(lastTime, Beatmap.Breaks[currentBreak].EndTime);
}
// Apply health adjustments

View File

@ -19,6 +19,11 @@ namespace osu.Game.Rulesets.Scoring
/// </summary>
public readonly double TimeOffset;
/// <summary>
/// The true gameplay rate at the time of the event.
/// </summary>
public readonly double? GameplayRate;
/// <summary>
/// The hit result.
/// </summary>
@ -46,12 +51,14 @@ namespace osu.Game.Rulesets.Scoring
/// </summary>
/// <param name="timeOffset">The time offset from the end of <paramref name="hitObject"/> at which the event occurs.</param>
/// <param name="result">The <see cref="HitResult"/>.</param>
/// <param name="gameplayRate">The true gameplay rate at the time of the event.</param>
/// <param name="hitObject">The <see cref="HitObject"/> that triggered the event.</param>
/// <param name="lastHitObject">The previous <see cref="HitObject"/>.</param>
/// <param name="position">A position corresponding to the event.</param>
public HitEvent(double timeOffset, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, [CanBeNull] Vector2? position)
public HitEvent(double timeOffset, double? gameplayRate, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, [CanBeNull] Vector2? position)
{
TimeOffset = timeOffset;
GameplayRate = gameplayRate;
Result = result;
HitObject = hitObject;
LastHitObject = lastHitObject;
@ -63,6 +70,6 @@ namespace osu.Game.Rulesets.Scoring
/// </summary>
/// <param name="positionOffset">The positional offset.</param>
/// <returns>The new <see cref="HitEvent"/>.</returns>
public HitEvent With(Vector2? positionOffset) => new HitEvent(TimeOffset, Result, HitObject, LastHitObject, positionOffset);
public HitEvent With(Vector2? positionOffset) => new HitEvent(TimeOffset, GameplayRate, Result, HitObject, LastHitObject, positionOffset);
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace osu.Game.Rulesets.Scoring
@ -18,7 +19,10 @@ namespace osu.Game.Rulesets.Scoring
/// </returns>
public static double? CalculateUnstableRate(this IEnumerable<HitEvent> hitEvents)
{
double[] timeOffsets = hitEvents.Where(affectsUnstableRate).Select(ev => ev.TimeOffset).ToArray();
Debug.Assert(hitEvents.All(ev => ev.GameplayRate != null));
// Division by gameplay rate is to account for TimeOffset scaling with gameplay rate.
double[] timeOffsets = hitEvents.Where(affectsUnstableRate).Select(ev => ev.TimeOffset / ev.GameplayRate!.Value).ToArray();
return 10 * standardDeviation(timeOffsets);
}

View File

@ -252,7 +252,7 @@ namespace osu.Game.Rulesets.Scoring
/// <param name="result">The <see cref="JudgementResult"/> to describe.</param>
/// <returns>The <see cref="HitEvent"/>.</returns>
protected virtual HitEvent CreateHitEvent(JudgementResult result)
=> new HitEvent(result.TimeOffset, result.Type, result.HitObject, lastHitObject, null);
=> new HitEvent(result.TimeOffset, result.GameplayRate, result.Type, result.HitObject, lastHitObject, null);
protected sealed override void RevertResultInternal(JudgementResult result)
{

View File

@ -50,7 +50,7 @@ namespace osu.Game.Scoring.Legacy
switch (rulesetId)
{
case 0:
return (long)Math.Round((objectCount * objectCount * 32.57 + 100000) * standardisedTotalScore / ScoreProcessor.MAX_SCORE);
return (long)Math.Round((Math.Pow(objectCount, 2) * 32.57 + 100000) * standardisedTotalScore / ScoreProcessor.MAX_SCORE);
case 1:
return (long)Math.Round((objectCount * 1109 + 100000) * standardisedTotalScore / ScoreProcessor.MAX_SCORE);

View File

@ -4,11 +4,9 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
@ -151,7 +149,7 @@ namespace osu.Game.Screens.Edit.Components.Menus
{
switch (item)
{
case EditorMenuItemSpacer spacer:
case OsuMenuItemSpacer spacer:
return new DrawableSpacer(spacer);
case StatefulMenuItem stateful:
@ -195,29 +193,6 @@ namespace osu.Game.Screens.Edit.Components.Menus
Foreground.Padding = new MarginPadding { Vertical = 2 };
}
}
private partial class DrawableSpacer : DrawableOsuMenuItem
{
public DrawableSpacer(MenuItem item)
: base(item)
{
Scale = new Vector2(1, 0.6f);
AddInternal(new Box
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = BackgroundColourHover,
RelativeSizeAxes = Axes.X,
Height = 2f,
Width = 0.8f,
});
}
protected override bool OnHover(HoverEvent e) => true;
protected override bool OnClick(ClickEvent e) => true;
}
}
}
}

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