1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-18 14:10:33 +08:00

Compare commits

..

269 Commits

203 changed files with 3528 additions and 949 deletions
+2 -2
View File
@@ -21,10 +21,10 @@
]
},
"ppy.localisationanalyser.tools": {
"version": "2023.712.0",
"version": "2023.1117.0",
"commands": [
"localisation"
]
}
}
}
}
+8 -12
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:
+2 -2
View File
@@ -59,7 +59,7 @@ The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of
In the case of simple issues, a direct PR is okay. However, if you decide to work on an existing issue which doesn't seem trivial, **please ask us first**. This way we can try to estimate if it is a good fit for you and provide the correct direction on how to address it. In addition, note that while we do not rule out external contributors from working on roadmapped issues, we will generally prefer to handle them ourselves unless they're not very time sensitive.
If you'd like to propose a subjective change to one of the visual aspects of the game, or there is a bigger task you'd like to work on, but there is no corresponding issue or discussion thread yet for it, **please open a discussion or issue first** to avoid wasted effort. This in particular applies if you want to work on [one of the available designs from the osu! public Figma library](https://www.figma.com/file/6m10GiGEncVFWmgOoSyakH/osu!-Figma-Library).
If you'd like to propose a subjective change to one of the visual aspects of the game, or there is a bigger task you'd like to work on, but there is no corresponding issue or discussion thread yet for it, **please open a discussion or issue first** to avoid wasted effort. This in particular applies if you want to work on [one of the available designs from the osu! Figma master library](https://www.figma.com/file/VIkXMYNPMtQem2RJg9k2iQ/Master-Library).
Aside from the above, below is a brief checklist of things to watch out when you're preparing your code changes:
@@ -85,4 +85,4 @@ If you're uncertain about some part of the codebase or some inner workings of th
- [Development roadmap](https://github.com/orgs/ppy/projects/7/views/6): What the core team is currently working on
- [`ppy/osu-framework` wiki](https://github.com/ppy/osu-framework/wiki): Contains introductory information about osu!framework, the bespoke 2D game framework we use for the game
- [`ppy/osu` wiki](https://github.com/ppy/osu/wiki): Contains articles about various technical aspects of the game
- [Public Figma library](https://www.figma.com/file/6m10GiGEncVFWmgOoSyakH/osu!-Figma-Library): Contains finished and draft designs for osu!
- [Figma master library](https://www.figma.com/file/VIkXMYNPMtQem2RJg9k2iQ/Master-Library): Contains finished and draft designs for osu!
+1 -1
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.1124.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
@@ -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)
{
}
}
@@ -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", () =>
@@ -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));
}
@@ -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),
@@ -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 })
}
}
}
@@ -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)
@@ -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
}
}
@@ -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)
@@ -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)
@@ -23,6 +23,22 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
{
}
public override void PreProcess()
{
IHasComboInformation? lastObj = null;
// For sanity, ensures that both the first hitobject and the first hitobject after a banana shower start a new combo.
// This is normally enforced by the legacy decoder, but is not enforced by the editor.
foreach (var obj in Beatmap.HitObjects.OfType<IHasComboInformation>())
{
if (obj is not BananaShower && (lastObj == null || lastObj is BananaShower))
obj.NewCombo = true;
lastObj = obj;
}
base.PreProcess();
}
public override void PostProcess()
{
base.PostProcess();
+2
View File
@@ -39,6 +39,8 @@ namespace osu.Game.Rulesets.Catch
public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor();
public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new CatchHealthProcessor(drainStartTime);
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new CatchBeatmapConverter(beatmap, this);
public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new CatchBeatmapProcessor(beatmap);
@@ -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.
@@ -155,6 +155,33 @@ namespace osu.Game.Rulesets.Catch.Objects
Scale = LegacyRulesetExtensions.CalculateScaleFromCircleSize(difficulty.CircleSize);
}
public void UpdateComboInformation(IHasComboInformation? lastObj)
{
// Note that this implementation is shared with the osu! ruleset's implementation.
// If a change is made here, OsuHitObject.cs should also be updated.
ComboIndex = lastObj?.ComboIndex ?? 0;
ComboIndexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0;
IndexInCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0;
if (this is BananaShower)
{
// For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so.
return;
}
// At decode time, the first hitobject in the beatmap and the first hitobject after a banana shower are both enforced to be a new combo,
// but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here.
if (NewCombo || lastObj == null || lastObj is BananaShower)
{
IndexInCurrentCombo = 0;
ComboIndex++;
ComboIndexWithOffsets += ComboOffset + 1;
if (lastObj != null)
lastObj.LastInCombo = true;
}
}
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
#region Hit object conversion
@@ -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;
@@ -0,0 +1,168 @@
// 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 osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Catch.Scoring
{
public partial class CatchHealthProcessor : DrainingHealthProcessor
{
public Action<string>? OnIterationFail;
public Action<string>? OnIterationSuccess;
private double lowestHpEver;
private double lowestHpEnd;
private double hpRecoveryAvailable;
private double hpMultiplierNormal;
public CatchHealthProcessor(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;
List<HitObject> allObjects = EnumerateHitObjects(Beatmap).Where(h => h is Fruit || h is Droplet || h is Banana).ToList();
for (int i = 0; i < allObjects.Count; i++)
{
HitObject h = allObjects[i];
while (currentBreak < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak].EndTime <= h.StartTime)
{
// If two hitobjects are separated by a break period, there is no drain for the full duration between the hitobjects.
// This differs from legacy (version < 8) beatmaps which continue draining until the break section is entered,
// but this shouldn't have a noticeable impact in practice.
lastTime = h.StartTime;
currentBreak++;
}
reduceHp(testDrop * (h.StartTime - lastTime));
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;
}
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) / allObjects.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.CreateJudgement().MaxResult);
currentHpUncapped += amount;
currentHp = Math.Max(0, Math.Min(1, currentHp + amount));
}
}
protected override double GetHealthIncreaseFor(JudgementResult result) => healthIncreaseFor(result.Type);
private double healthIncreaseFor(HitResult result)
{
double increase = 0;
switch (result)
{
case HitResult.SmallTickMiss:
return 0;
case HitResult.LargeTickMiss:
case HitResult.Miss:
return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.03, -0.125, -0.2);
case HitResult.SmallTickHit:
increase = 0.0015;
break;
case HitResult.LargeTickHit:
increase = 0.015;
break;
case HitResult.Great:
increase = 0.03;
break;
case HitResult.LargeBonus:
increase = 0.0025;
break;
}
return hpMultiplierNormal * increase;
}
}
}
@@ -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;
}
}
@@ -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();
}
}
}
}
@@ -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))
}),
@@ -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)));
}
@@ -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();
}
}
}
@@ -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))
};
@@ -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));
});
@@ -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)
@@ -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))
};
@@ -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();
@@ -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)
)
@@ -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),
@@ -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]
@@ -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))
})
@@ -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)),
};
@@ -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
{
@@ -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);
@@ -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
@@ -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),
@@ -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),
@@ -0,0 +1,93 @@
// 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 NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Scoring;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
public class TestSceneOsuHealthProcessor
{
[Test]
public void TestNoBreak()
{
OsuHealthProcessor hp = new OsuHealthProcessor(-1000);
hp.ApplyBeatmap(new Beatmap<OsuHitObject>
{
HitObjects =
{
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 2000 }
}
});
Assert.That(hp.DrainRate, Is.EqualTo(1.4E-5).Within(0.1E-5));
}
[Test]
public void TestSingleBreak()
{
OsuHealthProcessor hp = new OsuHealthProcessor(-1000);
hp.ApplyBeatmap(new Beatmap<OsuHitObject>
{
HitObjects =
{
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 2000 }
},
Breaks =
{
new BreakPeriod(500, 1500)
}
});
Assert.That(hp.DrainRate, Is.EqualTo(4.3E-5).Within(0.1E-5));
}
[Test]
public void TestOverlappingBreak()
{
OsuHealthProcessor hp = new OsuHealthProcessor(-1000);
hp.ApplyBeatmap(new Beatmap<OsuHitObject>
{
HitObjects =
{
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 2000 }
},
Breaks =
{
new BreakPeriod(500, 1400),
new BreakPeriod(750, 1500),
}
});
Assert.That(hp.DrainRate, Is.EqualTo(4.3E-5).Within(0.1E-5));
}
[Test]
public void TestSequentialBreak()
{
OsuHealthProcessor hp = new OsuHealthProcessor(-1000);
hp.ApplyBeatmap(new Beatmap<OsuHitObject>
{
HitObjects =
{
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 2000 }
},
Breaks =
{
new BreakPeriod(500, 1000),
new BreakPeriod(1000, 1500),
}
});
Assert.That(hp.DrainRate, Is.EqualTo(4.3E-5).Within(0.1E-5));
}
}
}
@@ -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),
@@ -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),
@@ -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),
@@ -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),
@@ -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)
@@ -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),
@@ -2,9 +2,11 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
@@ -19,6 +21,22 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
{
}
public override void PreProcess()
{
IHasComboInformation? lastObj = null;
// For sanity, ensures that both the first hitobject and the first hitobject after a spinner start a new combo.
// This is normally enforced by the legacy decoder, but is not enforced by the editor.
foreach (var obj in Beatmap.HitObjects.OfType<IHasComboInformation>())
{
if (obj is not Spinner && (lastObj == null || lastObj is Spinner))
obj.NewCombo = true;
lastObj = obj;
}
base.PreProcess();
}
public override void PostProcess()
{
base.PostProcess();
@@ -95,15 +113,15 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
{
int n = i;
/* We should check every note which has not yet got a stack.
* Consider the case we have two interwound stacks and this will make sense.
*
* o <-1 o <-2
* o <-3 o <-4
*
* We first process starting from 4 and handle 2,
* then we come backwards on the i loop iteration until we reach 3 and handle 1.
* 2 and 1 will be ignored in the i loop because they already have a stack value.
*/
* Consider the case we have two interwound stacks and this will make sense.
*
* o <-1 o <-2
* o <-3 o <-4
*
* We first process starting from 4 and handle 2,
* then we come backwards on the i loop iteration until we reach 3 and handle 1.
* 2 and 1 will be ignored in the i loop because they already have a stack value.
*/
OsuHitObject objectI = beatmap.HitObjects[i];
if (objectI.StackHeight != 0 || objectI is Spinner) continue;
@@ -111,9 +129,9 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
double stackThreshold = objectI.TimePreempt * beatmap.BeatmapInfo.StackLeniency;
/* If this object is a hitcircle, then we enter this "special" case.
* It either ends with a stack of hitcircles only, or a stack of hitcircles that are underneath a slider.
* Any other case is handled by the "is Slider" code below this.
*/
* It either ends with a stack of hitcircles only, or a stack of hitcircles that are underneath a slider.
* Any other case is handled by the "is Slider" code below this.
*/
if (objectI is HitCircle)
{
while (--n >= 0)
@@ -135,10 +153,10 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
}
/* This is a special case where hticircles are moved DOWN and RIGHT (negative stacking) if they are under the *last* slider in a stacked pattern.
* o==o <- slider is at original location
* o <- hitCircle has stack of -1
* o <- hitCircle has stack of -2
*/
* o==o <- slider is at original location
* o <- hitCircle has stack of -1
* o <- hitCircle has stack of -2
*/
if (objectN is Slider && Vector2Extensions.Distance(objectN.EndPosition, objectI.Position) < stack_distance)
{
int offset = objectI.StackHeight - objectN.StackHeight + 1;
@@ -169,8 +187,8 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
else if (objectI is Slider)
{
/* We have hit the first slider in a possible stack.
* From this point on, we ALWAYS stack positive regardless.
*/
* From this point on, we ALWAYS stack positive regardless.
*/
while (--n >= startIndex)
{
OsuHitObject objectN = beatmap.HitObjects[n];
@@ -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
}
@@ -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;
}
}
@@ -159,9 +159,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
if (allowSelection)
d.RequestSelection = selectionRequested;
d.DragStarted = dragStarted;
d.DragInProgress = dragInProgress;
d.DragEnded = dragEnded;
d.DragStarted = DragStarted;
d.DragInProgress = DragInProgress;
d.DragEnded = DragEnded;
}));
Connections.Add(new PathControlPointConnectionPiece<T>(hitObject, e.NewStartingIndex + i));
@@ -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;
@@ -270,7 +267,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private int draggedControlPointIndex;
private HashSet<PathControlPoint> selectedControlPoints;
private void dragStarted(PathControlPoint controlPoint)
public void DragStarted(PathControlPoint controlPoint)
{
dragStartPositions = hitObject.Path.ControlPoints.Select(point => point.Position).ToArray();
dragPathTypes = hitObject.Path.ControlPoints.Select(point => point.Type).ToArray();
@@ -282,7 +279,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
changeHandler?.BeginChange();
}
private void dragInProgress(DragEvent e)
public void DragInProgress(DragEvent e)
{
Vector2[] oldControlPoints = hitObject.Path.ControlPoints.Select(cp => cp.Position).ToArray();
var oldPosition = hitObject.Position;
@@ -344,7 +341,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
hitObject.Path.ControlPoints[i].Type = dragPathTypes[i];
}
private void dragEnded() => changeHandler?.EndChange();
public void DragEnded() => changeHandler?.EndChange();
#endregion
@@ -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);
@@ -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
}
}
}
@@ -39,9 +39,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
[CanBeNull]
protected PathControlPointVisualiser<Slider> ControlPointVisualiser { get; private set; }
[Resolved(CanBeNull = true)]
private IPositionSnapProvider positionSnapProvider { get; set; }
[Resolved(CanBeNull = true)]
private IDistanceSnapProvider distanceSnapProvider { get; set; }
@@ -191,21 +188,30 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
[CanBeNull]
private PathControlPoint placementControlPoint;
protected override bool OnDragStart(DragStartEvent e) => placementControlPoint != null;
protected override bool OnDragStart(DragStartEvent e)
{
if (placementControlPoint == null)
return base.OnDragStart(e);
ControlPointVisualiser?.DragStarted(placementControlPoint);
return true;
}
protected override void OnDrag(DragEvent e)
{
base.OnDrag(e);
if (placementControlPoint != null)
{
var result = positionSnapProvider?.FindSnappedPositionAndTime(ToScreenSpace(e.MousePosition));
placementControlPoint.Position = ToLocalSpace(result?.ScreenSpacePosition ?? ToScreenSpace(e.MousePosition)) - HitObject.Position;
}
ControlPointVisualiser?.DragInProgress(e);
}
protected override void OnMouseUp(MouseUpEvent e)
{
if (placementControlPoint != null)
{
if (IsDragged)
ControlPointVisualiser?.DragEnded();
placementControlPoint = null;
changeHandler?.EndChange();
}
@@ -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);
}
}
}
@@ -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;
@@ -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));
@@ -159,6 +159,33 @@ namespace osu.Game.Rulesets.Osu.Objects
Scale = LegacyRulesetExtensions.CalculateScaleFromCircleSize(difficulty.CircleSize, true);
}
public void UpdateComboInformation(IHasComboInformation? lastObj)
{
// Note that this implementation is shared with the osu!catch ruleset's implementation.
// If a change is made here, CatchHitObject.cs should also be updated.
ComboIndex = lastObj?.ComboIndex ?? 0;
ComboIndexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0;
IndexInCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0;
if (this is Spinner)
{
// For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so.
return;
}
// At decode time, the first hitobject in the beatmap and the first hitobject after a spinner are both enforced to be a new combo,
// but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here.
if (NewCombo || lastObj == null || lastObj is Spinner)
{
IndexInCurrentCombo = 0;
ComboIndex++;
ComboIndexWithOffsets += ComboOffset + 1;
if (lastObj != null)
lastObj.LastInCombo = true;
}
}
protected override HitWindows CreateHitWindows() => new OsuHitWindows();
}
}
+2
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);
@@ -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);
@@ -0,0 +1,212 @@
// 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.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];
while (currentBreak < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak].EndTime <= h.StartTime)
{
// If two hitobjects are separated by a break period, there is no drain for the full duration between the hitobjects.
// This differs from legacy (version < 8) beatmaps which continue draining until the break section is entered,
// but this shouldn't have a noticeable impact in practice.
lastTime = h.StartTime;
currentBreak++;
}
reduceHp(testDrop * (h.StartTime - lastTime));
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;
}
}
}
@@ -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()
@@ -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()
@@ -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()
+1 -1
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
}
@@ -12,6 +12,7 @@ using osu.Game.Beatmaps.Formats;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Beatmaps.Timing;
using osu.Game.IO;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Mods;
@@ -433,12 +434,12 @@ namespace osu.Game.Tests.Beatmaps.Formats
new OsuBeatmapProcessor(converted).PreProcess();
new OsuBeatmapProcessor(converted).PostProcess();
Assert.AreEqual(4, ((IHasComboInformation)converted.HitObjects.ElementAt(0)).ComboIndexWithOffsets);
Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(2)).ComboIndexWithOffsets);
Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(4)).ComboIndexWithOffsets);
Assert.AreEqual(6, ((IHasComboInformation)converted.HitObjects.ElementAt(6)).ComboIndexWithOffsets);
Assert.AreEqual(11, ((IHasComboInformation)converted.HitObjects.ElementAt(8)).ComboIndexWithOffsets);
Assert.AreEqual(14, ((IHasComboInformation)converted.HitObjects.ElementAt(11)).ComboIndexWithOffsets);
Assert.AreEqual(1, ((IHasComboInformation)converted.HitObjects.ElementAt(0)).ComboIndexWithOffsets);
Assert.AreEqual(2, ((IHasComboInformation)converted.HitObjects.ElementAt(2)).ComboIndexWithOffsets);
Assert.AreEqual(3, ((IHasComboInformation)converted.HitObjects.ElementAt(4)).ComboIndexWithOffsets);
Assert.AreEqual(4, ((IHasComboInformation)converted.HitObjects.ElementAt(6)).ComboIndexWithOffsets);
Assert.AreEqual(8, ((IHasComboInformation)converted.HitObjects.ElementAt(8)).ComboIndexWithOffsets);
Assert.AreEqual(9, ((IHasComboInformation)converted.HitObjects.ElementAt(11)).ComboIndexWithOffsets);
}
}
@@ -456,12 +457,12 @@ namespace osu.Game.Tests.Beatmaps.Formats
new CatchBeatmapProcessor(converted).PreProcess();
new CatchBeatmapProcessor(converted).PostProcess();
Assert.AreEqual(4, ((IHasComboInformation)converted.HitObjects.ElementAt(0)).ComboIndexWithOffsets);
Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(2)).ComboIndexWithOffsets);
Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(4)).ComboIndexWithOffsets);
Assert.AreEqual(6, ((IHasComboInformation)converted.HitObjects.ElementAt(6)).ComboIndexWithOffsets);
Assert.AreEqual(11, ((IHasComboInformation)converted.HitObjects.ElementAt(8)).ComboIndexWithOffsets);
Assert.AreEqual(14, ((IHasComboInformation)converted.HitObjects.ElementAt(11)).ComboIndexWithOffsets);
Assert.AreEqual(1, ((IHasComboInformation)converted.HitObjects.ElementAt(0)).ComboIndexWithOffsets);
Assert.AreEqual(2, ((IHasComboInformation)converted.HitObjects.ElementAt(2)).ComboIndexWithOffsets);
Assert.AreEqual(3, ((IHasComboInformation)converted.HitObjects.ElementAt(4)).ComboIndexWithOffsets);
Assert.AreEqual(4, ((IHasComboInformation)converted.HitObjects.ElementAt(6)).ComboIndexWithOffsets);
Assert.AreEqual(8, ((IHasComboInformation)converted.HitObjects.ElementAt(8)).ComboIndexWithOffsets);
Assert.AreEqual(9, ((IHasComboInformation)converted.HitObjects.ElementAt(11)).ComboIndexWithOffsets);
}
}
@@ -663,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);
@@ -808,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)));
@@ -827,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)));
@@ -837,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)));
@@ -856,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)));
@@ -870,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)));
@@ -881,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));
@@ -889,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)));
@@ -904,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)));
@@ -1016,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));
}
}
@@ -1032,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);
}
}
@@ -1051,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));
@@ -1093,5 +1094,67 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(hitObject.Samples.Select(s => s.Volume), Has.All.EqualTo(70));
}
}
[Test]
public void TestNewComboAfterBreak()
{
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("break-between-objects.osu"))
using (var stream = new LineBufferedReader(resStream))
{
var beatmap = decoder.Decode(stream);
Assert.That(((IHasCombo)beatmap.HitObjects[0]).NewCombo, Is.True);
Assert.That(((IHasCombo)beatmap.HitObjects[1]).NewCombo, Is.True);
Assert.That(((IHasCombo)beatmap.HitObjects[2]).NewCombo, Is.False);
}
}
/// <summary>
/// Test cases that involve a spinner between two hitobjects.
/// </summary>
[Test]
public void TestSpinnerNewComboBetweenObjects([Values("osu", "catch")] string rulesetName)
{
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("spinner-between-objects.osu"))
using (var stream = new LineBufferedReader(resStream))
{
Ruleset ruleset;
switch (rulesetName)
{
case "osu":
ruleset = new OsuRuleset();
break;
case "catch":
ruleset = new CatchRuleset();
break;
default:
throw new ArgumentOutOfRangeException(nameof(rulesetName), rulesetName, null);
}
var working = new TestWorkingBeatmap(decoder.Decode(stream));
var playable = working.GetPlayableBeatmap(ruleset.RulesetInfo, Array.Empty<Mod>());
// There's no good way to figure out these values other than to compare (in code) with osu!stable...
Assert.That(((IHasComboInformation)playable.HitObjects[0]).ComboIndexWithOffsets, Is.EqualTo(1));
Assert.That(((IHasComboInformation)playable.HitObjects[2]).ComboIndexWithOffsets, Is.EqualTo(2));
Assert.That(((IHasComboInformation)playable.HitObjects[3]).ComboIndexWithOffsets, Is.EqualTo(2));
Assert.That(((IHasComboInformation)playable.HitObjects[5]).ComboIndexWithOffsets, Is.EqualTo(3));
Assert.That(((IHasComboInformation)playable.HitObjects[6]).ComboIndexWithOffsets, Is.EqualTo(3));
Assert.That(((IHasComboInformation)playable.HitObjects[8]).ComboIndexWithOffsets, Is.EqualTo(4));
Assert.That(((IHasComboInformation)playable.HitObjects[9]).ComboIndexWithOffsets, Is.EqualTo(4));
Assert.That(((IHasComboInformation)playable.HitObjects[11]).ComboIndexWithOffsets, Is.EqualTo(5));
Assert.That(((IHasComboInformation)playable.HitObjects[12]).ComboIndexWithOffsets, Is.EqualTo(6));
Assert.That(((IHasComboInformation)playable.HitObjects[14]).ComboIndexWithOffsets, Is.EqualTo(7));
Assert.That(((IHasComboInformation)playable.HitObjects[15]).ComboIndexWithOffsets, Is.EqualTo(8));
Assert.That(((IHasComboInformation)playable.HitObjects[17]).ComboIndexWithOffsets, Is.EqualTo(9));
}
}
}
}
@@ -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)
{
}
}
@@ -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)
@@ -192,7 +192,8 @@ namespace osu.Game.Tests.Gameplay
AddStep("apply perfect hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = HitResult.Perfect }));
AddAssert("not failed", () => !processor.HasFailed);
AddStep($"apply {resultApplied.ToString().ToLowerInvariant()} hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = resultApplied }));
AddStep($"apply {resultApplied.ToString().ToLowerInvariant()} hit result",
() => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = resultApplied }));
AddAssert("failed", () => processor.HasFailed);
}
@@ -232,6 +233,84 @@ namespace osu.Game.Tests.Gameplay
assertHealthEqualTo(1);
}
[Test]
public void TestNoBreakDrainRate()
{
DrainingHealthProcessor hp = new DrainingHealthProcessor(-1000);
hp.ApplyBeatmap(new Beatmap<JudgeableHitObject>
{
HitObjects =
{
new JudgeableHitObject { StartTime = 0 },
new JudgeableHitObject { StartTime = 2000 }
}
});
Assert.That(hp.DrainRate, Is.EqualTo(4.5E-5).Within(0.1E-5));
}
[Test]
public void TestSingleBreakDrainRate()
{
DrainingHealthProcessor hp = new DrainingHealthProcessor(-1000);
hp.ApplyBeatmap(new Beatmap<JudgeableHitObject>
{
HitObjects =
{
new JudgeableHitObject { StartTime = 0 },
new JudgeableHitObject { StartTime = 2000 }
},
Breaks =
{
new BreakPeriod(500, 1500)
}
});
Assert.That(hp.DrainRate, Is.EqualTo(9.1E-5).Within(0.1E-5));
}
[Test]
public void TestOverlappingBreakDrainRate()
{
DrainingHealthProcessor hp = new DrainingHealthProcessor(-1000);
hp.ApplyBeatmap(new Beatmap<JudgeableHitObject>
{
HitObjects =
{
new JudgeableHitObject { StartTime = 0 },
new JudgeableHitObject { StartTime = 2000 }
},
Breaks =
{
new BreakPeriod(500, 1400),
new BreakPeriod(750, 1500),
}
});
Assert.That(hp.DrainRate, Is.EqualTo(9.1E-5).Within(0.1E-5));
}
[Test]
public void TestSequentialBreakDrainRate()
{
DrainingHealthProcessor hp = new DrainingHealthProcessor(-1000);
hp.ApplyBeatmap(new Beatmap<JudgeableHitObject>
{
HitObjects =
{
new JudgeableHitObject { StartTime = 0 },
new JudgeableHitObject { StartTime = 2000 }
},
Breaks =
{
new BreakPeriod(500, 1000),
new BreakPeriod(1000, 1500),
}
});
Assert.That(hp.DrainRate, Is.EqualTo(9.1E-5).Within(0.1E-5));
}
private Beatmap createBeatmap(double startTime, double endTime, params BreakPeriod[] breaks)
{
var beatmap = new Beatmap
@@ -94,9 +94,6 @@ namespace osu.Game.Tests.Gameplay
private class TestHitObjectWithCombo : ConvertHitObject, IHasComboInformation
{
public bool NewCombo { get; set; }
public int ComboOffset => 0;
public Bindable<int> IndexInCurrentComboBindable { get; } = new Bindable<int>();
public int IndexInCurrentCombo
@@ -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);
}
}
}
@@ -0,0 +1,15 @@
osu file format v14
[General]
Mode: 0
[Events]
2,200,1200
[TimingPoints]
0,307.692307692308,4,2,1,60,1,0
[HitObjects]
142,99,0,1,0,0:0:0:0:
323,88,3000,1,0,0:0:0:0:
323,88,4000,1,0,0:0:0:0:
@@ -3,30 +3,30 @@ osu file format v14
[HitObjects]
// Circle with combo offset (3)
255,193,1000,49,0,0:0:0:0:
// Combo index = 4
// Combo index = 1
// Spinner with new combo followed by circle with no new combo
256,192,2000,12,0,2000,0:0:0:0:
255,193,3000,1,0,0:0:0:0:
// Combo index = 5
// Combo index = 2
// Spinner without new combo followed by circle with no new combo
256,192,4000,8,0,5000,0:0:0:0:
255,193,6000,1,0,0:0:0:0:
// Combo index = 5
// Combo index = 3
// Spinner without new combo followed by circle with new combo
256,192,7000,8,0,8000,0:0:0:0:
255,193,9000,5,0,0:0:0:0:
// Combo index = 6
// Combo index = 4
// Spinner with new combo and offset (1) followed by circle with new combo and offset (3)
256,192,10000,28,0,11000,0:0:0:0:
255,193,12000,53,0,0:0:0:0:
// Combo index = 11
// Combo index = 8
// Spinner with new combo and offset (2) followed by slider with no new combo followed by circle with no new combo
256,192,13000,44,0,14000,0:0:0:0:
256,192,15000,8,0,16000,0:0:0:0:
255,193,17000,1,0,0:0:0:0:
// Combo index = 14
// Combo index = 9
@@ -0,0 +1,38 @@
osu file format v14
[General]
Mode: 0
[TimingPoints]
0,571.428571428571,4,2,1,5,1,0
[HitObjects]
// +C -> +C -> +C
104,95,0,5,0,0:0:0:0:
256,192,1000,12,0,2000,0:0:0:0:
178,171,3000,5,0,0:0:0:0:
// -C -> +C -> +C
178,171,4000,1,0,0:0:0:0:
256,192,5000,12,0,6000,0:0:0:0:
178,171,7000,5,0,0:0:0:0:
// -C -> -C -> +C
178,171,8000,1,0,0:0:0:0:
256,192,9000,8,0,10000,0:0:0:0:
178,171,11000,5,0,0:0:0:0:
// -C -> -C -> -C
178,171,12000,1,0,0:0:0:0:
256,192,13000,8,0,14000,0:0:0:0:
178,171,15000,1,0,0:0:0:0:
// +C -> -C -> -C
178,171,16000,5,0,0:0:0:0:
256,192,17000,8,0,18000,0:0:0:0:
178,171,19000,1,0,0:0:0:0:
// +C -> +C -> -C
178,171,20000,5,0,0:0:0:0:
256,192,21000,12,0,22000,0:0:0:0:
178,171,23000,1,0,0:0:0:0:
@@ -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)]
+20
View File
@@ -13,6 +13,7 @@ using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.IO;
using osu.Game.Skinning;
using osu.Game.Tests.Resources;
using SharpCompress.Archives.Zip;
namespace osu.Game.Tests.Skins.IO
@@ -21,6 +22,25 @@ namespace osu.Game.Tests.Skins.IO
{
#region Testing filename metadata inclusion
[TestCase("Archives/modified-classic-20220723.osk")]
[TestCase("Archives/modified-default-20230117.osk")]
[TestCase("Archives/modified-argon-20231106.osk")]
public Task TestImportModifiedSkinHasResources(string archive) => runSkinTest(async osu =>
{
using (var stream = TestResources.OpenResource(archive))
{
var imported = await loadSkinIntoOsu(osu, new ImportTask(stream, "skin.osk"));
// When the import filename doesn't match, it should be appended (and update the skin.ini).
var skinManager = osu.Dependencies.Get<SkinManager>();
skinManager.CurrentSkinInfo.Value = imported;
Assert.That(skinManager.CurrentSkin.Value.LayoutInfos.Count, Is.EqualTo(2));
}
});
[Test]
public Task TestSingleImportDifferentFilename() => runSkinTest(async osu =>
{
@@ -15,6 +15,7 @@ using osu.Game.IO.Archives;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Skinning;
using osu.Game.Skinning.Components;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Skins
@@ -102,6 +103,20 @@ namespace osu.Game.Tests.Skins
}
}
[Test]
public void TestDeserialiseModifiedArgon()
{
using (var stream = TestResources.OpenResource("Archives/modified-argon-20231106.osk"))
using (var storage = new ZipArchiveReader(stream))
{
var skin = new TestSkin(new SkinInfo(), null, storage);
Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2));
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(10));
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(PlayerName)));
}
}
[Test]
public void TestDeserialiseModifiedClassic()
{
@@ -134,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)
{
}
@@ -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)
{
}
@@ -181,6 +181,54 @@ namespace osu.Game.Tests.Visual.Background
AddStep("restore default beatmap", () => Beatmap.SetDefault());
}
[Test]
public void TestBeatmapBackgroundWithStoryboardUnloadedOnSuspension()
{
BackgroundScreenBeatmap nestedScreen = null;
setSupporter(true);
setSourceMode(BackgroundSource.BeatmapWithStoryboard);
AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithStoryboard());
AddAssert("background changed", () => screen.CheckLastLoadChange() == true);
AddUntilStep("wait for beatmap background to be loaded", () => getCurrentBackground()?.GetType() == typeof(BeatmapBackgroundWithStoryboard));
AddUntilStep("storyboard present", () => screen.ChildrenOfType<DrawableStoryboard>().SingleOrDefault()?.IsLoaded == true);
AddStep("push new background to stack", () => stack.Push(nestedScreen = new BackgroundScreenBeatmap(Beatmap.Value)));
AddUntilStep("wait for screen to load", () => nestedScreen.IsLoaded && nestedScreen.IsCurrentScreen());
AddUntilStep("storyboard unloaded", () => !screen.ChildrenOfType<DrawableStoryboard>().Any());
AddStep("go back", () => screen.MakeCurrent());
AddUntilStep("storyboard reloaded", () => screen.ChildrenOfType<DrawableStoryboard>().SingleOrDefault()?.IsLoaded == true);
}
[Test]
public void TestBeatmapBackgroundWithStoryboardButBeatmapHasNone()
{
BackgroundScreenBeatmap nestedScreen = null;
setSupporter(true);
setSourceMode(BackgroundSource.BeatmapWithStoryboard);
AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground());
AddAssert("background changed", () => screen.CheckLastLoadChange() == true);
AddUntilStep("wait for beatmap background to be loaded", () => getCurrentBackground()?.GetType() == typeof(BeatmapBackgroundWithStoryboard));
AddUntilStep("no storyboard loaded", () => !screen.ChildrenOfType<DrawableStoryboard>().Any());
AddStep("push new background to stack", () => stack.Push(nestedScreen = new BackgroundScreenBeatmap(Beatmap.Value)));
AddUntilStep("wait for screen to load", () => nestedScreen.IsLoaded && nestedScreen.IsCurrentScreen());
AddUntilStep("still no storyboard", () => !screen.ChildrenOfType<DrawableStoryboard>().Any());
AddStep("go back", () => screen.MakeCurrent());
AddUntilStep("still no storyboard", () => !screen.ChildrenOfType<DrawableStoryboard>().Any());
}
[Test]
public void TestBackgroundTypeSwitch()
{
@@ -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)
}
}
};
@@ -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"),
}
},
@@ -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),
@@ -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);
}
}
}
@@ -52,6 +52,12 @@ namespace osu.Game.Tests.Visual.Gameplay
if (healthDisplay.IsNotNull())
healthDisplay.BarHeight.Value = val;
});
AddSliderStep("Width", 0, 1f, 0.98f, val =>
{
if (healthDisplay.IsNotNull())
healthDisplay.Width = val;
});
}
[Test]
@@ -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;
}
});
@@ -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) },
},
});
@@ -335,6 +335,40 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("value is less than default", () => hitErrorMeter.JudgementLineThickness.Value < hitErrorMeter.JudgementLineThickness.Default);
}
[Test]
public void TestCopyPaste()
{
AddStep("paste", () =>
{
InputManager.PressKey(Key.LControl);
InputManager.Key(Key.V);
InputManager.ReleaseKey(Key.LControl);
});
// no assertions. just make sure nothing crashes.
AddStep("select bar hit error blueprint", () =>
{
var blueprint = skinEditor.ChildrenOfType<SkinBlueprint>().First(b => b.Item is BarHitErrorMeter);
skinEditor.SelectedComponents.Clear();
skinEditor.SelectedComponents.Add(blueprint.Item);
});
AddStep("copy", () =>
{
InputManager.PressKey(Key.LControl);
InputManager.Key(Key.C);
InputManager.ReleaseKey(Key.LControl);
});
AddStep("paste", () =>
{
InputManager.PressKey(Key.LControl);
InputManager.Key(Key.V);
InputManager.ReleaseKey(Key.LControl);
});
AddAssert("three hit error meters present",
() => skinEditor.ChildrenOfType<SkinBlueprint>().Count(b => b.Item is BarHitErrorMeter),
() => Is.EqualTo(3));
}
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
private partial class TestSkinEditorChangeHandler : SkinEditorChangeHandler
@@ -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)));
@@ -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,
});
}
@@ -239,7 +239,7 @@ namespace osu.Game.Tests.Visual.Navigation
{
AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType<DialogOverlay>().SingleOrDefault() != null);
AddStep("open editor", () => Game.ChildrenOfType<ButtonSystem>().Single().OnEdit.Invoke());
AddStep("open editor", () => Game.ChildrenOfType<ButtonSystem>().Single().OnEditBeatmap?.Invoke());
AddUntilStep("wait for editor", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.IsLoaded);
AddStep("click on file", () =>
{
@@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestFromSongSelectWithFilter([Values] ScorePresentType type)
{
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo.Invoke());
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo?.Invoke());
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
AddStep("filter to nothing", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).FilterControl.CurrentTextSearch.Value = "fdsajkl;fgewq");
@@ -110,7 +110,7 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestFromSongSelectWithConvertRulesetChange([Values] ScorePresentType type)
{
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo.Invoke());
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo?.Invoke());
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
AddStep("set convert to false", () => Game.LocalConfig.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
@@ -122,7 +122,7 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestFromSongSelect([Values] ScorePresentType type)
{
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo.Invoke());
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo?.Invoke());
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
var firstImport = importScore(1);
@@ -135,7 +135,7 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestFromSongSelectDifferentRuleset([Values] ScorePresentType type)
{
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo.Invoke());
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo?.Invoke());
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
var firstImport = importScore(1);
@@ -64,7 +64,7 @@ namespace osu.Game.Tests.Visual.Online
new[] { "Plain", "This is plain comment" },
new[] { "Pinned", "This is pinned comment" },
new[] { "Link", "Please visit https://osu.ppy.sh" },
new[] { "Big Image", "![](Backgrounds/bg1)" },
new[] { "Big Image", "![](Backgrounds/bg1 \"Big Image\")" },
new[] { "Small Image", "![](Cursor/cursortrail)" },
new[]
{
@@ -0,0 +1,86 @@
// 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.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Testing;
using osu.Game.Graphics.Cursor;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Users;
using osu.Game.Users.Drawables;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Online
{
public partial class TestSceneUserClickableAvatar : OsuManualInputManagerTestScene
{
[SetUp]
public void SetUp() => Schedule(() =>
{
Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Spacing = new Vector2(10f),
Children = new[]
{
generateUser(@"peppy", 2, CountryCode.AU, @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", false, "99EB47"),
generateUser(@"flyte", 3103765, CountryCode.JP, @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg", true),
generateUser(@"joshika39", 17032217, CountryCode.RS, @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", false),
new UpdateableAvatar(),
new UpdateableAvatar()
},
};
});
[Test]
public void TestClickableAvatarHover()
{
AddStep("hover avatar with user panel", () => InputManager.MoveMouseTo(this.ChildrenOfType<ClickableAvatar>().ElementAt(1)));
AddUntilStep("wait for tooltip to show", () => this.ChildrenOfType<ClickableAvatar.UserCardTooltip>().FirstOrDefault()?.State.Value == Visibility.Visible);
AddStep("hover out", () => InputManager.MoveMouseTo(new Vector2(0)));
AddUntilStep("wait for tooltip to hide", () => this.ChildrenOfType<ClickableAvatar.UserCardTooltip>().FirstOrDefault()?.State.Value == Visibility.Hidden);
AddStep("hover avatar without user panel", () => InputManager.MoveMouseTo(this.ChildrenOfType<ClickableAvatar>().ElementAt(0)));
AddUntilStep("wait for tooltip to show", () => this.ChildrenOfType<OsuTooltipContainer.OsuTooltip>().FirstOrDefault()?.State.Value == Visibility.Visible);
AddStep("hover out", () => InputManager.MoveMouseTo(new Vector2(0)));
AddUntilStep("wait for tooltip to hide", () => this.ChildrenOfType<OsuTooltipContainer.OsuTooltip>().FirstOrDefault()?.State.Value == Visibility.Hidden);
}
private Drawable generateUser(string username, int id, CountryCode countryCode, string cover, bool showPanel, string? color = null)
{
var user = new APIUser
{
Username = username,
Id = id,
CountryCode = countryCode,
CoverUrl = cover,
Colour = color ?? "000000",
Status =
{
Value = new UserStatusOnline()
},
};
return new ClickableAvatar(user, showPanel)
{
Width = 50,
Height = 50,
CornerRadius = 10,
Masking = true,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Radius = 1,
Colour = Color4.Black.Opacity(0.2f),
},
};
}
}
}
@@ -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",
@@ -10,7 +10,6 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
@@ -298,7 +297,7 @@ This is a line after the fenced code block!
{
public LinkInline Link;
public override MarkdownTextFlowContainer CreateTextFlow() => new TestMarkdownTextFlowContainer
public override OsuMarkdownTextFlowContainer CreateTextFlow() => new TestMarkdownTextFlowContainer
{
UrlAdded = link => Link = link,
};
@@ -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;
@@ -67,14 +67,15 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("Enter mode", performEnterMode);
}
[TestCase(Key.P, true)]
[TestCase(Key.M, true)]
[TestCase(Key.L, true)]
[TestCase(Key.E, false)]
[TestCase(Key.D, false)]
[TestCase(Key.Q, false)]
[TestCase(Key.O, false)]
public void TestShortcutKeys(Key key, bool entersPlay)
[TestCase(Key.P, Key.P)]
[TestCase(Key.M, Key.P)]
[TestCase(Key.L, Key.P)]
[TestCase(Key.B, Key.E)]
[TestCase(Key.S, Key.E)]
[TestCase(Key.D, null)]
[TestCase(Key.Q, null)]
[TestCase(Key.O, null)]
public void TestShortcutKeys(Key key, Key? subMenuEnterKey)
{
int activationCount = -1;
AddStep("set up action", () =>
@@ -96,8 +97,12 @@ namespace osu.Game.Tests.Visual.UserInterface
buttons.OnPlaylists = action;
break;
case Key.E:
buttons.OnEdit = action;
case Key.B:
buttons.OnEditBeatmap = action;
break;
case Key.S:
buttons.OnEditSkin = action;
break;
case Key.D:
@@ -117,10 +122,10 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep($"press {key}", () => InputManager.Key(key));
AddAssert("state is top level", () => buttons.State == ButtonSystemState.TopLevel);
if (entersPlay)
if (subMenuEnterKey != null)
{
AddStep("press P", () => InputManager.Key(Key.P));
AddAssert("state is play", () => buttons.State == ButtonSystemState.Play);
AddStep($"press {subMenuEnterKey}", () => InputManager.Key(subMenuEnterKey.Value));
AddAssert("state is not top menu", () => buttons.State != ButtonSystemState.TopLevel);
}
AddStep($"press {key}", () => InputManager.Key(key));
@@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.UserInterface
@@ -153,7 +154,7 @@ namespace osu.Game.Tests.Visual.UserInterface
public TestTitle()
{
Title = "title";
IconTexture = "Icons/changelog";
Icon = HexaconsIcons.Devtools;
}
}
}
+4 -9
View File
@@ -1,8 +1,6 @@
// 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 osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Objects;
@@ -26,8 +24,7 @@ namespace osu.Game.Beatmaps
{
difficulty = value;
if (beatmapInfo != null)
beatmapInfo.Difficulty = difficulty.Clone();
beatmapInfo.Difficulty = difficulty.Clone();
}
}
@@ -40,8 +37,7 @@ namespace osu.Game.Beatmaps
{
beatmapInfo = value;
if (beatmapInfo?.Difficulty != null)
Difficulty = beatmapInfo.Difficulty.Clone();
Difficulty = beatmapInfo.Difficulty.Clone();
}
}
@@ -119,12 +115,11 @@ namespace osu.Game.Beatmaps
IBeatmap IBeatmap.Clone() => Clone();
public Beatmap<T> Clone() => (Beatmap<T>)MemberwiseClone();
public override string ToString() => BeatmapInfo.ToString();
}
public class Beatmap : Beatmap<HitObject>
{
public new Beatmap Clone() => (Beatmap)base.Clone();
public override string ToString() => BeatmapInfo?.ToString() ?? base.ToString();
}
}
-6
View File
@@ -24,12 +24,6 @@ namespace osu.Game.Beatmaps
foreach (var obj in Beatmap.HitObjects.OfType<IHasComboInformation>())
{
if (lastObj == null)
{
// first hitobject should always be marked as a new combo for sanity.
obj.NewCombo = true;
}
obj.UpdateComboInformation(lastObj);
lastObj = obj;
}
@@ -74,7 +74,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Statistics
#region Tooltip implementation
public virtual ITooltip GetCustomTooltip() => null;
public virtual ITooltip GetCustomTooltip() => null!;
public virtual object TooltipContent => null;
#endregion
@@ -93,6 +93,8 @@ namespace osu.Game.Beatmaps.Formats
// The parsing order of hitobjects matters in mania difficulty calculation
this.beatmap.HitObjects = this.beatmap.HitObjects.OrderBy(h => h.StartTime).ToList();
postProcessBreaks(this.beatmap);
foreach (var hitObject in this.beatmap.HitObjects)
{
applyDefaults(hitObject);
@@ -100,6 +102,27 @@ namespace osu.Game.Beatmaps.Formats
}
}
/// <summary>
/// Processes the beatmap such that a new combo is started the first hitobject following each break.
/// </summary>
private void postProcessBreaks(Beatmap beatmap)
{
int currentBreak = 0;
bool forceNewCombo = false;
foreach (var h in beatmap.HitObjects.OfType<ConvertHitObject>())
{
while (currentBreak < beatmap.Breaks.Count && beatmap.Breaks[currentBreak].EndTime < h.StartTime)
{
forceNewCombo = true;
currentBreak++;
}
h.NewCombo |= forceNewCombo;
forceNewCombo = false;
}
}
private void applyDefaults(HitObject hitObject)
{
DifficultyControlPoint difficultyControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.DifficultyPointAt(hitObject.StartTime) ?? DifficultyControlPoint.DEFAULT;

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