1
0
mirror of https://github.com/ppy/osu.git synced 2026-06-05 17:54:14 +08:00

Merge branch 'master' into feat/block-users

This commit is contained in:
Dean Herbert
2025-04-23 13:55:18 +09:00
Unverified
316 changed files with 8559 additions and 5102 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.321.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.419.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
@@ -1,6 +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.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
@@ -16,26 +17,30 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
int fruits = HitObjects.Count(s => s is Fruit);
int juiceStreams = HitObjects.Count(s => s is JuiceStream);
int bananaShowers = HitObjects.Count(s => s is BananaShower);
int sum = Math.Max(1, fruits + juiceStreams);
return new[]
{
new BeatmapStatistic
{
Name = @"Fruit Count",
Name = @"Fruits",
Content = fruits.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
BarDisplayLength = fruits / (float)sum,
},
new BeatmapStatistic
{
Name = @"Juice Stream Count",
Name = @"Juice Streams",
Content = juiceStreams.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
BarDisplayLength = juiceStreams / (float)sum,
},
new BeatmapStatistic
{
Name = @"Banana Shower Count",
Name = @"Banana Showers",
Content = bananaShowers.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners),
BarDisplayLength = Math.Min(bananaShowers / 10f, 1),
}
};
}
@@ -75,6 +75,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Setup
{
Caption = EditorSetupStrings.BaseVelocity,
HintText = EditorSetupStrings.BaseVelocityDescription,
KeyboardStep = 0.1f,
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
{
Default = 1.4,
@@ -89,6 +90,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Setup
{
Caption = EditorSetupStrings.TickRate,
HintText = EditorSetupStrings.TickRateDescription,
KeyboardStep = 1,
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
{
Default = 1,
@@ -0,0 +1,187 @@
// 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.Rulesets.Mania.Mods;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
namespace osu.Game.Rulesets.Mania.Tests
{
[TestFixture]
public class ManiaFilterCriteriaTest
{
[TestCase]
public void TestKeysEqualSingleValue()
{
var criteria = new ManiaFilterCriteria();
criteria.TryParseCustomKeywordCriteria("keys", Operator.Equal, "1");
Assert.True(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }),
new FilterCriteria()));
Assert.False(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }),
new FilterCriteria()));
Assert.False(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }),
new FilterCriteria()));
Assert.True(criteria.Matches(
new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }),
new FilterCriteria
{
Mods = [new ManiaModKey1()]
}));
}
[TestCase]
public void TestKeysEqualMultipleValues()
{
var criteria = new ManiaFilterCriteria();
criteria.TryParseCustomKeywordCriteria("keys", Operator.Equal, "1,3,5,7");
Assert.True(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }),
new FilterCriteria()));
Assert.False(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }),
new FilterCriteria()));
Assert.True(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }),
new FilterCriteria()));
Assert.True(criteria.Matches(
new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }),
new FilterCriteria
{
Mods = [new ManiaModKey1()]
}));
}
[TestCase]
public void TestKeysNotEqualSingleValue()
{
var criteria = new ManiaFilterCriteria();
criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "1");
Assert.False(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }),
new FilterCriteria()));
Assert.True(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }),
new FilterCriteria()));
Assert.True(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }),
new FilterCriteria()));
Assert.False(criteria.Matches(
new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }),
new FilterCriteria
{
Mods = [new ManiaModKey1()]
}));
}
[TestCase]
public void TestKeysNotEqualMultipleValues()
{
var criteria = new ManiaFilterCriteria();
criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "1,3,5,7");
Assert.False(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }),
new FilterCriteria()));
Assert.True(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }),
new FilterCriteria()));
Assert.False(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }),
new FilterCriteria()));
Assert.False(criteria.Matches(
new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }),
new FilterCriteria
{
Mods = [new ManiaModKey1()]
}));
}
[TestCase]
public void TestKeysGreaterOrEqualThan()
{
var criteria = new ManiaFilterCriteria();
criteria.TryParseCustomKeywordCriteria("keys", Operator.GreaterOrEqual, "4");
Assert.False(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }),
new FilterCriteria()));
Assert.False(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }),
new FilterCriteria()));
Assert.True(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 4 }),
new FilterCriteria()));
Assert.True(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 5 }),
new FilterCriteria()));
Assert.True(criteria.Matches(
new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 3 }),
new FilterCriteria
{
Mods = [new ManiaModKey7()]
}));
}
[TestCase]
public void TestFilterIntersection()
{
var criteria = new ManiaFilterCriteria();
criteria.TryParseCustomKeywordCriteria("keys", Operator.Greater, "4");
criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "7");
Assert.False(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }),
new FilterCriteria()));
Assert.False(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 4 }),
new FilterCriteria()));
Assert.True(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 5 }),
new FilterCriteria()));
Assert.False(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 7 }),
new FilterCriteria()));
Assert.True(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 9 }),
new FilterCriteria()));
}
[TestCase]
public void TestInvalidFilters()
{
var criteria = new ManiaFilterCriteria();
Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.Equal, "some text"));
Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "4,some text"));
Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.GreaterOrEqual, "4,5,6"));
}
}
}
@@ -0,0 +1,470 @@
// 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.ControlPoints;
using osu.Game.Replays;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Mania.Tests
{
[Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")]
public partial class TestSceneLegacyReplayPlayback : LegacyReplayPlaybackTestScene
{
protected override Ruleset CreateRuleset() => new ManiaRuleset();
protected override string? ExportLocation => null;
private static readonly object[][] score_v2_test_cases =
{
// With respect to notation,
// square brackets `[]` represent *closed* or *inclusive* bounds,
// while round brackets `()` represent *open* or *exclusive* bounds.
// Note that mania hitwindows are heavily idiosyncratic,
// and if you *think* a number here is wrong, probably double check.
// Known issues / complexities:
// - There is a disparate set of hitwindow ranges for: score V1 non-converts, score V1 converts, and score V2 (regardless of convert)
// - It is NEVER POSSIBLE to get a MEH result when late; exceeding the OK hit windows will result in a MISS.
// Additionally, the OK hit window when late is EXCLUSIVE / OPEN rather than INCLUSIVE / CLOSED.
// Relevant stable source: https://github.com/peppy/osu-stable-reference/blob/996648fba06baf4e7d2e0b248959399444017895/osu!/GameplayElements/HitObjectManagerMania.cs#L737-L751
// - There is also a seemingly mania-specific issue wherein key inputs registered before time instant 0 get truncated to time 0,
// which is why the beatmaps used below make sure not to cross that boundary (the note starts at t=300ms).
// This is not an issue in osu! or taiko.
// The source of this behaviour has not been investigated in detail.
// OD = 5 test cases.
// PERFECT hit window is [ -19ms, 19ms]
// GREAT hit window is [ -49ms, 49ms]
// GOOD hit window is [ -82ms, 82ms]
// OK hit window is [-112ms, 112ms) <- not a typo, this side of the interval is OPEN!
// MEH hit window is [-136ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit!
new object[] { 5f, -18d, HitResult.Perfect },
new object[] { 5f, -19d, HitResult.Perfect },
new object[] { 5f, -20d, HitResult.Great },
new object[] { 5f, -21d, HitResult.Great },
new object[] { 5f, -48d, HitResult.Great },
new object[] { 5f, -49d, HitResult.Great },
new object[] { 5f, -50d, HitResult.Good },
new object[] { 5f, -51d, HitResult.Good },
new object[] { 5f, -81d, HitResult.Good },
new object[] { 5f, -82d, HitResult.Good },
new object[] { 5f, -83d, HitResult.Ok },
new object[] { 5f, -84d, HitResult.Ok },
new object[] { 5f, -111d, HitResult.Ok },
new object[] { 5f, -112d, HitResult.Ok },
new object[] { 5f, -113d, HitResult.Meh },
new object[] { 5f, -114d, HitResult.Meh },
new object[] { 5f, -135d, HitResult.Meh },
new object[] { 5f, -136d, HitResult.Meh },
new object[] { 5f, -137d, HitResult.Miss },
new object[] { 5f, -138d, HitResult.Miss },
new object[] { 5f, 111d, HitResult.Ok },
new object[] { 5f, 112d, HitResult.Miss },
new object[] { 5f, 113d, HitResult.Miss },
new object[] { 5f, 114d, HitResult.Miss },
new object[] { 5f, 135d, HitResult.Miss },
new object[] { 5f, 136d, HitResult.Miss },
new object[] { 5f, 137d, HitResult.Miss },
new object[] { 5f, 138d, HitResult.Miss },
// OD = 9.3 test cases.
// PERFECT hit window is [ -14ms, 14ms]
// GREAT hit window is [ -36ms, 36ms]
// GOOD hit window is [ -69ms, 69ms]
// OK hit window is [ -99ms, 99ms) <- not a typo, this side of the interval is OPEN!
// MEH hit window is [-123ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit!
new object[] { 9.3f, 13d, HitResult.Perfect },
new object[] { 9.3f, 14d, HitResult.Perfect },
new object[] { 9.3f, 15d, HitResult.Great },
new object[] { 9.3f, 16d, HitResult.Great },
new object[] { 9.3f, 35d, HitResult.Great },
new object[] { 9.3f, 36d, HitResult.Great },
new object[] { 9.3f, 37d, HitResult.Good },
new object[] { 9.3f, 38d, HitResult.Good },
new object[] { 9.3f, 68d, HitResult.Good },
new object[] { 9.3f, 69d, HitResult.Good },
new object[] { 9.3f, 70d, HitResult.Ok },
new object[] { 9.3f, 71d, HitResult.Ok },
new object[] { 9.3f, 98d, HitResult.Ok },
new object[] { 9.3f, 99d, HitResult.Miss },
new object[] { 9.3f, 100d, HitResult.Miss },
new object[] { 9.3f, 101d, HitResult.Miss },
new object[] { 9.3f, 122d, HitResult.Miss },
new object[] { 9.3f, 123d, HitResult.Miss },
new object[] { 9.3f, 124d, HitResult.Miss },
new object[] { 9.3f, 125d, HitResult.Miss },
new object[] { 9.3f, -98d, HitResult.Ok },
new object[] { 9.3f, -99d, HitResult.Ok },
new object[] { 9.3f, -100d, HitResult.Meh },
new object[] { 9.3f, -101d, HitResult.Meh },
new object[] { 9.3f, -122d, HitResult.Meh },
new object[] { 9.3f, -123d, HitResult.Meh },
new object[] { 9.3f, -124d, HitResult.Miss },
new object[] { 9.3f, -125d, HitResult.Miss },
};
private static readonly object[][] score_v1_non_convert_test_cases =
{
// OD = 5 test cases.
// PERFECT hit window is [ -16ms, 16ms]
// GREAT hit window is [ -49ms, 49ms]
// GOOD hit window is [ -82ms, 82ms]
// OK hit window is [-112ms, 112ms) <- not a typo, this side of the interval is OPEN!
// MEH hit window is [-136ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit!
new object[] { 5f, -15d, HitResult.Perfect },
new object[] { 5f, -16d, HitResult.Perfect },
new object[] { 5f, -17d, HitResult.Great },
new object[] { 5f, -18d, HitResult.Great },
new object[] { 5f, -48d, HitResult.Great },
new object[] { 5f, -49d, HitResult.Great },
new object[] { 5f, -50d, HitResult.Good },
new object[] { 5f, -51d, HitResult.Good },
new object[] { 5f, -81d, HitResult.Good },
new object[] { 5f, -82d, HitResult.Good },
new object[] { 5f, -83d, HitResult.Ok },
new object[] { 5f, -84d, HitResult.Ok },
new object[] { 5f, -111d, HitResult.Ok },
new object[] { 5f, -112d, HitResult.Ok },
new object[] { 5f, -113d, HitResult.Meh },
new object[] { 5f, -114d, HitResult.Meh },
new object[] { 5f, -135d, HitResult.Meh },
new object[] { 5f, -136d, HitResult.Meh },
new object[] { 5f, -137d, HitResult.Miss },
new object[] { 5f, -138d, HitResult.Miss },
new object[] { 5f, 111d, HitResult.Ok },
new object[] { 5f, 112d, HitResult.Miss },
new object[] { 5f, 113d, HitResult.Miss },
new object[] { 5f, 114d, HitResult.Miss },
new object[] { 5f, 135d, HitResult.Miss },
new object[] { 5f, 136d, HitResult.Miss },
new object[] { 5f, 137d, HitResult.Miss },
new object[] { 5f, 138d, HitResult.Miss },
// OD = 9.3 test cases.
// PERFECT hit window is [ -16ms, 16ms]
// GREAT hit window is [ -36ms, 36ms]
// GOOD hit window is [ -69ms, 69ms]
// OK hit window is [ -99ms, 99ms) <- not a typo, this side of the interval is OPEN!
// MEH hit window is [-123ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit!
new object[] { 9.3f, 15d, HitResult.Perfect },
new object[] { 9.3f, 16d, HitResult.Perfect },
new object[] { 9.3f, 17d, HitResult.Great },
new object[] { 9.3f, 18d, HitResult.Great },
new object[] { 9.3f, 35d, HitResult.Great },
new object[] { 9.3f, 36d, HitResult.Great },
new object[] { 9.3f, 37d, HitResult.Good },
new object[] { 9.3f, 38d, HitResult.Good },
new object[] { 9.3f, 68d, HitResult.Good },
new object[] { 9.3f, 69d, HitResult.Good },
new object[] { 9.3f, 70d, HitResult.Ok },
new object[] { 9.3f, 71d, HitResult.Ok },
new object[] { 9.3f, 98d, HitResult.Ok },
new object[] { 9.3f, 99d, HitResult.Miss },
new object[] { 9.3f, 100d, HitResult.Miss },
new object[] { 9.3f, 101d, HitResult.Miss },
new object[] { 9.3f, 122d, HitResult.Miss },
new object[] { 9.3f, 123d, HitResult.Miss },
new object[] { 9.3f, 124d, HitResult.Miss },
new object[] { 9.3f, 125d, HitResult.Miss },
new object[] { 9.3f, -98d, HitResult.Ok },
new object[] { 9.3f, -99d, HitResult.Ok },
new object[] { 9.3f, -100d, HitResult.Meh },
new object[] { 9.3f, -101d, HitResult.Meh },
new object[] { 9.3f, -122d, HitResult.Meh },
new object[] { 9.3f, -123d, HitResult.Meh },
new object[] { 9.3f, -124d, HitResult.Miss },
new object[] { 9.3f, -125d, HitResult.Miss },
// OD = 3.1 test cases.
// PERFECT hit window is [ -16ms, 16ms]
// GREAT hit window is [ -54ms, 54ms]
// GOOD hit window is [ -87ms, 87ms]
// OK hit window is [-117ms, 117ms) <- not a typo, this side of the interval is OPEN!
// MEH hit window is [-141ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit!
new object[] { 3.1f, 15d, HitResult.Perfect },
new object[] { 3.1f, 16d, HitResult.Perfect },
new object[] { 3.1f, 17d, HitResult.Great },
new object[] { 3.1f, 18d, HitResult.Great },
new object[] { 3.1f, 53d, HitResult.Great },
new object[] { 3.1f, 54d, HitResult.Great },
new object[] { 3.1f, 55d, HitResult.Good },
new object[] { 3.1f, 56d, HitResult.Good },
new object[] { 3.1f, 86d, HitResult.Good },
new object[] { 3.1f, 87d, HitResult.Good },
new object[] { 3.1f, 88d, HitResult.Ok },
new object[] { 3.1f, 89d, HitResult.Ok },
new object[] { 3.1f, 116d, HitResult.Ok },
new object[] { 3.1f, 117d, HitResult.Miss },
new object[] { 3.1f, 118d, HitResult.Miss },
new object[] { 3.1f, 119d, HitResult.Miss },
new object[] { 3.1f, 140d, HitResult.Miss },
new object[] { 3.1f, 141d, HitResult.Miss },
new object[] { 3.1f, 142d, HitResult.Miss },
new object[] { 3.1f, 143d, HitResult.Miss },
new object[] { 3.1f, -116d, HitResult.Ok },
new object[] { 3.1f, -117d, HitResult.Ok },
new object[] { 3.1f, -118d, HitResult.Meh },
new object[] { 3.1f, -119d, HitResult.Meh },
new object[] { 3.1f, -140d, HitResult.Meh },
new object[] { 3.1f, -141d, HitResult.Meh },
new object[] { 3.1f, -142d, HitResult.Miss },
new object[] { 3.1f, -143d, HitResult.Miss },
};
private static readonly object[][] score_v1_convert_test_cases =
{
// OD = 5 test cases.
// PERFECT hit window is [ -16ms, 16ms]
// GREAT hit window is [ -34ms, 34ms]
// GOOD hit window is [ -67ms, 67ms]
// OK hit window is [ -97ms, 97ms) <- not a typo, this side of the interval is OPEN!
// MEH hit window is [-121ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit!
new object[] { 5f, -15d, HitResult.Perfect },
new object[] { 5f, -16d, HitResult.Perfect },
new object[] { 5f, -17d, HitResult.Great },
new object[] { 5f, -18d, HitResult.Great },
new object[] { 5f, -33d, HitResult.Great },
new object[] { 5f, -34d, HitResult.Great },
new object[] { 5f, -35d, HitResult.Good },
new object[] { 5f, -36d, HitResult.Good },
new object[] { 5f, -66d, HitResult.Good },
new object[] { 5f, -67d, HitResult.Good },
new object[] { 5f, -68d, HitResult.Ok },
new object[] { 5f, -69d, HitResult.Ok },
new object[] { 5f, -96d, HitResult.Ok },
new object[] { 5f, -97d, HitResult.Ok },
new object[] { 5f, -98d, HitResult.Meh },
new object[] { 5f, -99d, HitResult.Meh },
new object[] { 5f, -120d, HitResult.Meh },
new object[] { 5f, -121d, HitResult.Meh },
new object[] { 5f, -122d, HitResult.Miss },
new object[] { 5f, -123d, HitResult.Miss },
new object[] { 5f, 96d, HitResult.Ok },
new object[] { 5f, 97d, HitResult.Miss },
new object[] { 5f, 98d, HitResult.Miss },
new object[] { 5f, 99d, HitResult.Miss },
new object[] { 5f, 120d, HitResult.Miss },
new object[] { 5f, 121d, HitResult.Miss },
new object[] { 5f, 122d, HitResult.Miss },
new object[] { 5f, 123d, HitResult.Miss },
// OD = 3.1 test cases.
// PERFECT hit window is [ -16ms, 16ms]
// GREAT hit window is [ -47ms, 47ms]
// GOOD hit window is [ -77ms, 77ms]
// OK hit window is [ -97ms, 97ms) <- not a typo, this side of the interval is OPEN!
// MEH hit window is [-121ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit!
new object[] { 3.1f, 15d, HitResult.Perfect },
new object[] { 3.1f, 16d, HitResult.Perfect },
new object[] { 3.1f, 17d, HitResult.Great },
new object[] { 3.1f, 18d, HitResult.Great },
new object[] { 3.1f, 46d, HitResult.Great },
new object[] { 3.1f, 47d, HitResult.Great },
new object[] { 3.1f, 48d, HitResult.Good },
new object[] { 3.1f, 49d, HitResult.Good },
new object[] { 3.1f, 76d, HitResult.Good },
new object[] { 3.1f, 77d, HitResult.Good },
new object[] { 3.1f, 78d, HitResult.Ok },
new object[] { 3.1f, 79d, HitResult.Ok },
new object[] { 3.1f, 96d, HitResult.Ok },
new object[] { 3.1f, 97d, HitResult.Miss },
new object[] { 3.1f, 98d, HitResult.Miss },
new object[] { 3.1f, 99d, HitResult.Miss },
new object[] { 3.1f, 120d, HitResult.Miss },
new object[] { 3.1f, 121d, HitResult.Miss },
new object[] { 3.1f, 122d, HitResult.Miss },
new object[] { 3.1f, 123d, HitResult.Miss },
new object[] { 3.1f, -96d, HitResult.Ok },
new object[] { 3.1f, -97d, HitResult.Ok },
new object[] { 3.1f, -98d, HitResult.Meh },
new object[] { 3.1f, -99d, HitResult.Meh },
new object[] { 3.1f, -120d, HitResult.Meh },
new object[] { 3.1f, -121d, HitResult.Meh },
new object[] { 3.1f, -122d, HitResult.Miss },
new object[] { 3.1f, -123d, HitResult.Miss },
};
[TestCaseSource(nameof(score_v2_test_cases))]
public void TestHitWindowTreatmentWithScoreV2(float overallDifficulty, double hitOffset, HitResult expectedResult)
{
const double note_time = 300;
var cpi = new ControlPointInfo();
cpi.Add(0, new TimingControlPoint { BeatLength = 1000 });
var beatmap = new ManiaBeatmap(new StageDefinition(1))
{
HitObjects =
{
new Note
{
StartTime = note_time,
Column = 0,
}
},
Difficulty = new BeatmapDifficulty
{
OverallDifficulty = overallDifficulty,
CircleSize = 1,
},
BeatmapInfo =
{
Ruleset = new ManiaRuleset().RulesetInfo,
},
ControlPointInfo = cpi,
};
var replay = new Replay
{
Frames =
{
new ManiaReplayFrame(0),
new ManiaReplayFrame(note_time + hitOffset, ManiaAction.Key1),
new ManiaReplayFrame(note_time + hitOffset + 20),
}
};
var score = new Score
{
Replay = replay,
ScoreInfo = new ScoreInfo
{
Ruleset = CreateRuleset().RulesetInfo,
Mods = [new ModScoreV2()]
}
};
RunTest($@"SV2 single note @ OD{overallDifficulty}", beatmap, $@"SV2 {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
}
[TestCaseSource(nameof(score_v1_non_convert_test_cases))]
public void TestHitWindowTreatmentWithScoreV1NonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult)
{
const double note_time = 300;
var cpi = new ControlPointInfo();
cpi.Add(0, new TimingControlPoint { BeatLength = 1000 });
var beatmap = new ManiaBeatmap(new StageDefinition(1))
{
HitObjects =
{
new Note
{
StartTime = note_time,
Column = 0,
}
},
Difficulty = new BeatmapDifficulty
{
OverallDifficulty = overallDifficulty,
CircleSize = 1,
},
BeatmapInfo =
{
Ruleset = new ManiaRuleset().RulesetInfo,
},
ControlPointInfo = cpi,
};
var replay = new Replay
{
Frames =
{
new ManiaReplayFrame(0),
new ManiaReplayFrame(note_time + hitOffset, ManiaAction.Key1),
new ManiaReplayFrame(note_time + hitOffset + 20),
}
};
var score = new Score
{
Replay = replay,
ScoreInfo = new ScoreInfo
{
Ruleset = CreateRuleset().RulesetInfo,
}
};
RunTest($@"SV1 single note @ OD{overallDifficulty}", beatmap, $@"SV1 {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
}
[TestCaseSource(nameof(score_v1_convert_test_cases))]
public void TestHitWindowTreatmentWithScoreV1Convert(float overallDifficulty, double hitOffset, HitResult expectedResult)
{
const double note_time = 300;
var cpi = new ControlPointInfo();
cpi.Add(0, new TimingControlPoint { BeatLength = 1000 });
var beatmap = new Beatmap
{
HitObjects =
{
new FakeCircle
{
StartTime = note_time,
}
},
Difficulty = new BeatmapDifficulty
{
OverallDifficulty = overallDifficulty,
},
BeatmapInfo =
{
Ruleset = new RulesetInfo { OnlineID = 0 }
},
ControlPointInfo = cpi,
};
var replay = new Replay
{
Frames =
{
new ManiaReplayFrame(0),
new ManiaReplayFrame(note_time + hitOffset, ManiaAction.Key1),
new ManiaReplayFrame(note_time + hitOffset + 20),
}
};
var score = new Score
{
Replay = replay,
ScoreInfo = new ScoreInfo
{
Ruleset = CreateRuleset().RulesetInfo,
Mods = [new ManiaModKey1()],
}
};
RunTest($@"SV1 convert single note @ OD{overallDifficulty}", beatmap, $@"SV1 convert {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
}
private class FakeCircle : HitObject, IHasPosition
{
public float X
{
get => Position.X;
set => Position = new Vector2(value, Position.Y);
}
public float Y
{
get => Position.Y;
set => Position = new Vector2(Position.X, value);
}
public Vector2 Position { get; set; }
}
}
}
@@ -0,0 +1,149 @@
// 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.Replays;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
{
[Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")]
public partial class TestSceneReplayStability : ReplayStabilityTestScene
{
private static readonly object[][] test_cases =
{
// With respect to notation,
// square brackets `[]` represent *closed* or *inclusive* bounds,
// while round brackets `()` represent *open* or *exclusive* bounds.
// OD = 5 test cases.
// PERFECT hit window is [ -19.4ms, 19.4ms]
// GREAT hit window is [ -49.0ms, 49.0ms]
// GOOD hit window is [ -82.0ms, 82.0ms]
// OK hit window is [-112.0ms, 112.0ms]
// MEH hit window is [-136.0ms, 136.0ms]
// MISS hit window is [-173.0ms, 173.0ms]
new object[] { 5f, -19d, HitResult.Perfect },
new object[] { 5f, -19.2d, HitResult.Perfect },
new object[] { 5f, -19.38d, HitResult.Perfect },
// new object[] { 5f, -19.4d, HitResult.Perfect }, <- in theory this should work, in practice it does not (fails even before encode & rounding due to floating point precision issues)
new object[] { 5f, -19.44d, HitResult.Great },
new object[] { 5f, -19.7d, HitResult.Great },
new object[] { 5f, -20d, HitResult.Great },
new object[] { 5f, -48d, HitResult.Great },
new object[] { 5f, -48.4d, HitResult.Great },
new object[] { 5f, -48.7d, HitResult.Great },
new object[] { 5f, -49d, HitResult.Great },
new object[] { 5f, -49.2d, HitResult.Good },
new object[] { 5f, -49.7d, HitResult.Good },
new object[] { 5f, -50d, HitResult.Good },
new object[] { 5f, -81d, HitResult.Good },
new object[] { 5f, -81.2d, HitResult.Good },
new object[] { 5f, -81.7d, HitResult.Good },
new object[] { 5f, -82d, HitResult.Good },
new object[] { 5f, -82.2d, HitResult.Ok },
new object[] { 5f, -82.7d, HitResult.Ok },
new object[] { 5f, -83d, HitResult.Ok },
new object[] { 5f, -111d, HitResult.Ok },
new object[] { 5f, -111.2d, HitResult.Ok },
new object[] { 5f, -111.7d, HitResult.Ok },
new object[] { 5f, -112d, HitResult.Ok },
new object[] { 5f, -112.2d, HitResult.Meh },
new object[] { 5f, -112.7d, HitResult.Meh },
new object[] { 5f, -113d, HitResult.Meh },
new object[] { 5f, -135d, HitResult.Meh },
new object[] { 5f, -135.2d, HitResult.Meh },
new object[] { 5f, -135.8d, HitResult.Meh },
new object[] { 5f, -136d, HitResult.Meh },
new object[] { 5f, -136.2d, HitResult.Miss },
new object[] { 5f, -136.7d, HitResult.Miss },
new object[] { 5f, -137d, HitResult.Miss },
// OD = 9.3 test cases.
// PERFECT hit window is [ -14.67ms, 14.67ms]
// GREAT hit window is [ -36.10ms, 36.10ms]
// GOOD hit window is [ -69.10ms, 69.10ms]
// OK hit window is [ -99.10ms, 99.10ms]
// MEH hit window is [-123.10ms, 123.10ms]
// MISS hit window is [-160.10ms, 160.10ms]
new object[] { 9.3f, 14d, HitResult.Perfect },
new object[] { 9.3f, 14.2d, HitResult.Perfect },
new object[] { 9.3f, 14.6d, HitResult.Perfect },
// new object[] { 9.3f, 14.67d, HitResult.Perfect }, <- in theory this should work, in practice it does not (fails even before encode & rounding due to floating point precision issues)
new object[] { 9.3f, 14.7d, HitResult.Great },
new object[] { 9.3f, 15d, HitResult.Great },
new object[] { 9.3f, 35d, HitResult.Great },
new object[] { 9.3f, 35.3d, HitResult.Great },
new object[] { 9.3f, 35.8d, HitResult.Great },
new object[] { 9.3f, 36.05d, HitResult.Great },
new object[] { 9.3f, 36.3d, HitResult.Good },
new object[] { 9.3f, 36.7d, HitResult.Good },
new object[] { 9.3f, 37d, HitResult.Good },
new object[] { 9.3f, 68d, HitResult.Good },
new object[] { 9.3f, 68.4d, HitResult.Good },
new object[] { 9.3f, 68.9d, HitResult.Good },
new object[] { 9.3f, 69.07d, HitResult.Good },
new object[] { 9.3f, 69.25d, HitResult.Ok },
new object[] { 9.3f, 69.85d, HitResult.Ok },
new object[] { 9.3f, 70d, HitResult.Ok },
new object[] { 9.3f, 98d, HitResult.Ok },
new object[] { 9.3f, 98.3d, HitResult.Ok },
new object[] { 9.3f, 98.6d, HitResult.Ok },
new object[] { 9.3f, 99d, HitResult.Ok },
new object[] { 9.3f, 99.3d, HitResult.Meh },
new object[] { 9.3f, 99.7d, HitResult.Meh },
new object[] { 9.3f, 100d, HitResult.Meh },
new object[] { 9.3f, 122d, HitResult.Meh },
new object[] { 9.3f, 122.34d, HitResult.Meh },
new object[] { 9.3f, 122.57d, HitResult.Meh },
new object[] { 9.3f, 123.04d, HitResult.Meh },
new object[] { 9.3f, 123.45d, HitResult.Miss },
new object[] { 9.3f, 123.95d, HitResult.Miss },
new object[] { 9.3f, 124d, HitResult.Miss },
};
[TestCaseSource(nameof(test_cases))]
public void TestHitWindowStability(float overallDifficulty, double hitOffset, HitResult expectedResult)
{
const double note_time = 100;
var beatmap = new ManiaBeatmap(new StageDefinition(1))
{
HitObjects =
{
new Note
{
StartTime = note_time,
Column = 0,
}
},
Difficulty = new BeatmapDifficulty
{
OverallDifficulty = overallDifficulty,
CircleSize = 1,
},
BeatmapInfo =
{
Ruleset = new ManiaRuleset().RulesetInfo,
},
};
var replay = new Replay
{
Frames =
{
new ManiaReplayFrame(0),
new ManiaReplayFrame(note_time + hitOffset, ManiaAction.Key1),
new ManiaReplayFrame(note_time + hitOffset + 20),
}
};
RunTest(beatmap, replay, [expectedResult]);
}
}
}
@@ -36,20 +36,23 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
{
int notes = HitObjects.Count(s => s is Note);
int holdNotes = HitObjects.Count(s => s is HoldNote);
int sum = Math.Max(1, notes + holdNotes);
return new[]
{
new BeatmapStatistic
{
Name = @"Note Count",
Name = @"Notes",
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
Content = notes.ToString(),
BarDisplayLength = notes / (float)sum,
},
new BeatmapStatistic
{
Name = @"Hold Note Count",
Name = @"Hold Notes",
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
Content = holdNotes.ToString(),
BarDisplayLength = holdNotes / (float)sum,
},
};
}
@@ -1,7 +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.
using System;
using osu.Framework.Configuration.Tracking;
using osu.Game.Configuration;
using osu.Game.Localisation;
@@ -25,17 +24,6 @@ namespace osu.Game.Rulesets.Mania.Configuration
SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, false);
SetDefault(ManiaRulesetSetting.MobileLayout, ManiaMobileLayout.Portrait);
#pragma warning disable CS0618
// Although obsolete, this is still required to populate the bindable from the database in case migration is required.
SetDefault<double?>(ManiaRulesetSetting.ScrollTime, null);
if (Get<double?>(ManiaRulesetSetting.ScrollTime) is double scrollTime)
{
SetValue(ManiaRulesetSetting.ScrollSpeed, Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime));
SetValue<double?>(ManiaRulesetSetting.ScrollTime, null);
}
#pragma warning restore CS0618
}
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
@@ -52,8 +40,6 @@ namespace osu.Game.Rulesets.Mania.Configuration
public enum ManiaRulesetSetting
{
[Obsolete("Use ScrollSpeed instead.")] // Can be removed 2023-11-30
ScrollTime,
ScrollSpeed,
ScrollDirection,
TimingBasedNoteColouring,
@@ -89,6 +89,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
{
Caption = EditorSetupStrings.BaseVelocity,
HintText = EditorSetupStrings.BaseVelocityDescription,
KeyboardStep = 0.1f,
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
{
Default = 1.4,
@@ -103,6 +104,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
{
Caption = EditorSetupStrings.TickRate,
HintText = EditorSetupStrings.TickRateDescription,
KeyboardStep = 1,
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
{
Default = 1,
+58 -5
View File
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.Rulesets.Filter;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Mods;
@@ -17,20 +18,72 @@ namespace osu.Game.Rulesets.Mania
{
public class ManiaFilterCriteria : IRulesetFilterCriteria
{
private FilterCriteria.OptionalRange<float> keys;
private readonly HashSet<int> includedKeyCounts = Enumerable.Range(1, LegacyBeatmapDecoder.MAX_MANIA_KEY_COUNT).ToHashSet();
public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria)
{
return !keys.HasFilter || keys.IsInRange(ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), criteria.Mods));
int keyCount = ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), criteria.Mods);
return includedKeyCounts.Contains(keyCount);
}
public bool TryParseCustomKeywordCriteria(string key, Operator op, string value)
public bool TryParseCustomKeywordCriteria(string key, Operator op, string strValues)
{
switch (key)
{
case "key":
case "keys":
return FilterQueryParser.TryUpdateCriteriaRange(ref keys, op, value);
{
var keyCounts = new HashSet<int>();
foreach (string strValue in strValues.Split(','))
{
if (!int.TryParse(strValue, out int keyCount))
return false;
keyCounts.Add(keyCount);
}
int? singleKeyCount = keyCounts.Count == 1 ? keyCounts.Single() : null;
switch (op)
{
case Operator.Equal:
includedKeyCounts.IntersectWith(keyCounts);
return true;
case Operator.NotEqual:
includedKeyCounts.ExceptWith(keyCounts);
return true;
case Operator.Less:
if (singleKeyCount == null) return false;
includedKeyCounts.RemoveWhere(k => k >= singleKeyCount.Value);
return true;
case Operator.LessOrEqual:
if (singleKeyCount == null) return false;
includedKeyCounts.RemoveWhere(k => k > singleKeyCount.Value);
return true;
case Operator.Greater:
if (singleKeyCount == null) return false;
includedKeyCounts.RemoveWhere(k => k <= singleKeyCount.Value);
return true;
case Operator.GreaterOrEqual:
if (singleKeyCount == null) return false;
includedKeyCounts.RemoveWhere(k => k < singleKeyCount.Value);
return true;
default:
return false;
}
}
}
return false;
@@ -38,7 +91,7 @@ namespace osu.Game.Rulesets.Mania
public bool FilterMayChangeFromMods(ValueChangedEvent<IReadOnlyList<Mod>> mods)
{
if (keys.HasFilter)
if (includedKeyCounts.Count != LegacyBeatmapDecoder.MAX_MANIA_KEY_COUNT)
{
// Interpreting as the Mod type is required for equality comparison.
HashSet<Mod> oldSet = mods.OldValue.OfType<ManiaKeyMod>().AsEnumerable<Mod>().ToHashSet();
@@ -60,8 +60,9 @@ namespace osu.Game.Rulesets.Mania.UI
private readonly BindableDouble configScrollSpeed = new BindableDouble();
private readonly Bindable<ManiaMobileLayout> mobileLayout = new Bindable<ManiaMobileLayout>();
public double TargetTimeRange { get; protected set; }
private double currentTimeRange;
protected double TargetTimeRange;
// Stores the current speed adjustment active in gameplay.
private readonly Track speedAdjustmentTrack = new TrackVirtual(0);
@@ -109,7 +110,13 @@ namespace osu.Game.Rulesets.Mania.UI
configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true);
Config.BindWith(ManiaRulesetSetting.ScrollSpeed, configScrollSpeed);
configScrollSpeed.BindValueChanged(speed => TargetTimeRange = ComputeScrollTime(speed.NewValue));
configScrollSpeed.BindValueChanged(speed =>
{
if (!AllowScrollSpeedAdjustment)
return;
TargetTimeRange = ComputeScrollTime(speed.NewValue);
});
TimeRange.Value = TargetTimeRange = currentTimeRange = ComputeScrollTime(configScrollSpeed.Value);
@@ -0,0 +1,118 @@
// 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.ControlPoints;
using osu.Game.Replays;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
[Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")]
public partial class TestSceneLegacyReplayPlayback : LegacyReplayPlaybackTestScene
{
protected override Ruleset CreateRuleset() => new OsuRuleset();
protected override string? ExportLocation => null;
private static readonly object[][] test_cases =
{
// With respect to notation,
// square brackets `[]` represent *closed* or *inclusive* bounds,
// while round brackets `()` represent *open* or *exclusive* bounds.
// Additionally, note that offsets provided in double will be rounded to the nearest integer.
// OD = 5 test cases.
// GREAT hit window is ( -50ms, 50ms)
// OK hit window is (-100ms, 100ms)
// MEH hit window is (-150ms, 150ms)
new object[] { 5f, 48d, HitResult.Great },
new object[] { 5f, 49d, HitResult.Great },
new object[] { 5f, 50d, HitResult.Ok },
new object[] { 5f, 51d, HitResult.Ok },
new object[] { 5f, 98d, HitResult.Ok },
new object[] { 5f, 99d, HitResult.Ok },
new object[] { 5f, 100d, HitResult.Meh },
new object[] { 5f, 101d, HitResult.Meh },
new object[] { 5f, 148d, HitResult.Meh },
new object[] { 5f, 149d, HitResult.Meh },
new object[] { 5f, 150d, HitResult.Miss },
new object[] { 5f, 151d, HitResult.Miss },
// OD = 5.7 test cases.
// GREAT hit window is ( -45ms, 45ms)
// OK hit window is ( -94ms, 94ms)
// MEH hit window is (-143ms, 143ms)
new object[] { 5.7f, 43d, HitResult.Great },
new object[] { 5.7f, 44d, HitResult.Great },
new object[] { 5.7f, 45d, HitResult.Ok },
new object[] { 5.7f, 46d, HitResult.Ok },
new object[] { 5.7f, 92d, HitResult.Ok },
new object[] { 5.7f, 93d, HitResult.Ok },
new object[] { 5.7f, 94d, HitResult.Meh },
new object[] { 5.7f, 95d, HitResult.Meh },
new object[] { 5.7f, 141d, HitResult.Meh },
new object[] { 5.7f, 142d, HitResult.Meh },
new object[] { 5.7f, 143d, HitResult.Miss },
new object[] { 5.7f, 144d, HitResult.Miss },
};
[TestCaseSource(nameof(test_cases))]
public void TestHitWindowTreatment(float overallDifficulty, double hitOffset, HitResult expectedResult)
{
const double hit_circle_time = 100;
var cpi = new ControlPointInfo();
cpi.Add(0, new TimingControlPoint { BeatLength = 1000 });
var beatmap = new OsuBeatmap
{
HitObjects =
{
new HitCircle
{
StartTime = hit_circle_time,
Position = OsuPlayfield.BASE_SIZE / 2
}
},
Difficulty = new BeatmapDifficulty { OverallDifficulty = overallDifficulty },
BeatmapInfo =
{
Ruleset = new OsuRuleset().RulesetInfo,
},
ControlPointInfo = cpi,
};
var replay = new Replay
{
Frames =
{
// required for correct playback in stable
new OsuReplayFrame(0, new Vector2(256, -500)),
new OsuReplayFrame(0, new Vector2(256, -500)),
new OsuReplayFrame(0, OsuPlayfield.BASE_SIZE / 2),
new OsuReplayFrame(hit_circle_time + hitOffset, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton),
new OsuReplayFrame(hit_circle_time + hitOffset + 20, OsuPlayfield.BASE_SIZE / 2),
}
};
var score = new Score
{
Replay = replay,
ScoreInfo = new ScoreInfo
{
Ruleset = CreateRuleset().RulesetInfo,
}
};
RunTest($@"single circle @ OD{overallDifficulty}", beatmap, $@"{hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
}
}
}
@@ -0,0 +1,113 @@
// 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.Replays;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
{
[Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")]
public partial class TestSceneReplayStability : ReplayStabilityTestScene
{
private static readonly object[][] test_cases =
{
// With respect to notation,
// square brackets `[]` represent *closed* or *inclusive* bounds,
// while round brackets `()` represent *open* or *exclusive* bounds.
// OD = 5 test cases.
// GREAT hit window is [ -50ms, 50ms]
// OK hit window is [-100ms, 100ms]
// MEH hit window is [-150ms, 150ms]
// MISS hit window is [-400ms, 400ms]
new object[] { 5f, 49d, HitResult.Great },
new object[] { 5f, 49.2d, HitResult.Great },
new object[] { 5f, 49.7d, HitResult.Great },
new object[] { 5f, 50d, HitResult.Great },
new object[] { 5f, 50.4d, HitResult.Ok },
new object[] { 5f, 50.9d, HitResult.Ok },
new object[] { 5f, 51d, HitResult.Ok },
new object[] { 5f, 99d, HitResult.Ok },
new object[] { 5f, 99.2d, HitResult.Ok },
new object[] { 5f, 99.7d, HitResult.Ok },
new object[] { 5f, 100d, HitResult.Ok },
new object[] { 5f, 100.4d, HitResult.Meh },
new object[] { 5f, 100.9d, HitResult.Meh },
new object[] { 5f, 101d, HitResult.Meh },
new object[] { 5f, 149d, HitResult.Meh },
new object[] { 5f, 149.2d, HitResult.Meh },
new object[] { 5f, 149.7d, HitResult.Meh },
new object[] { 5f, 150d, HitResult.Meh },
new object[] { 5f, 150.4d, HitResult.Miss },
new object[] { 5f, 150.9d, HitResult.Miss },
new object[] { 5f, 151d, HitResult.Miss },
// OD = 5.7 test cases.
// GREAT hit window is [ -45.8ms, 45.8ms]
// OK hit window is [ -94.4ms, 94.4ms]
// MEH hit window is [-143.0ms, 143.0ms]
// MISS hit window is [-400.0ms, 400.0ms]
new object[] { 5.7f, 45d, HitResult.Great },
new object[] { 5.7f, 45.2d, HitResult.Great },
new object[] { 5.7f, 45.8d, HitResult.Great },
new object[] { 5.7f, 45.9d, HitResult.Ok },
new object[] { 5.7f, 46d, HitResult.Ok },
new object[] { 5.7f, 46.4d, HitResult.Ok },
new object[] { 5.7f, 94d, HitResult.Ok },
new object[] { 5.7f, 94.2d, HitResult.Ok },
new object[] { 5.7f, 94.4d, HitResult.Ok },
new object[] { 5.7f, 94.48d, HitResult.Ok },
new object[] { 5.7f, 94.9d, HitResult.Meh },
new object[] { 5.7f, 95d, HitResult.Meh },
new object[] { 5.7f, 95.4d, HitResult.Meh },
new object[] { 5.7f, 142d, HitResult.Meh },
new object[] { 5.7f, 142.7d, HitResult.Meh },
new object[] { 5.7f, 143d, HitResult.Meh },
new object[] { 5.7f, 143.4d, HitResult.Miss },
new object[] { 5.7f, 143.9d, HitResult.Miss },
new object[] { 5.7f, 144d, HitResult.Miss },
};
[TestCaseSource(nameof(test_cases))]
public void TestHitWindowStability(float overallDifficulty, double hitOffset, HitResult expectedResult)
{
const double hit_circle_time = 100;
var beatmap = new OsuBeatmap
{
HitObjects =
{
new HitCircle
{
StartTime = hit_circle_time,
Position = OsuPlayfield.BASE_SIZE / 2
}
},
Difficulty = new BeatmapDifficulty { OverallDifficulty = overallDifficulty },
BeatmapInfo =
{
Ruleset = new OsuRuleset().RulesetInfo,
},
};
var replay = new Replay
{
Frames =
{
new OsuReplayFrame(0, OsuPlayfield.BASE_SIZE / 2),
new OsuReplayFrame(hit_circle_time + hitOffset, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton),
new OsuReplayFrame(hit_circle_time + hitOffset + 20, OsuPlayfield.BASE_SIZE / 2),
}
};
RunTest(beatmap, replay, [expectedResult]);
}
}
}
@@ -86,9 +86,12 @@ namespace osu.Game.Rulesets.Osu.Tests
[Test]
public void TestSpinningSamplePitchShift()
{
PausableSkinnableSound spinSample = null;
AddStep("Add spinner", () => SetContents(_ => testSingle(5, true, 4000)));
AddUntilStep("Pitch starts low", () => getSpinningSample().Frequency.Value < 0.8);
AddUntilStep("Pitch increases", () => getSpinningSample().Frequency.Value > 0.8);
AddUntilStep("wait for spin sample", () => (spinSample = getSpinningSample()) != null);
AddUntilStep("Pitch starts low", () => spinSample.Frequency.Value < 0.8);
AddUntilStep("Pitch increases", () => spinSample.Frequency.Value > 0.8);
PausableSkinnableSound getSpinningSample() =>
drawableSpinner.ChildrenOfType<PausableSkinnableSound>().FirstOrDefault(s => s.Samples.Any(i => i.LookupNames.Any(l => l.Contains("spinnerspin"))));
+8 -4
View File
@@ -1,10 +1,10 @@
// 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.Resources.Localisation.Web;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Beatmaps
@@ -16,26 +16,30 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
int circles = HitObjects.Count(c => c is HitCircle);
int sliders = HitObjects.Count(s => s is Slider);
int spinners = HitObjects.Count(s => s is Spinner);
int sum = Math.Max(1, circles + sliders);
return new[]
{
new BeatmapStatistic
{
Name = BeatmapsetsStrings.ShowStatsCountCircles,
Name = "Circles",
Content = circles.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
BarDisplayLength = circles / (float)sum,
},
new BeatmapStatistic
{
Name = BeatmapsetsStrings.ShowStatsCountSliders,
Name = "Sliders",
Content = sliders.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
BarDisplayLength = sliders / (float)sum,
},
new BeatmapStatistic
{
Name = @"Spinner Count",
Name = @"Spinners",
Content = spinners.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners),
BarDisplayLength = Math.Min(spinners / 10f, 1),
}
};
}
@@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Edit
Current = new BindableNumber<int>(3)
{
MinValue = 3,
MaxValue = 10,
MaxValue = 32,
Precision = 1,
},
Instantaneous = true
@@ -91,6 +91,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup
{
Caption = EditorSetupStrings.BaseVelocity,
HintText = EditorSetupStrings.BaseVelocityDescription,
KeyboardStep = 0.1f,
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
{
Default = 1.4,
@@ -105,6 +106,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup
{
Caption = EditorSetupStrings.TickRate,
HintText = EditorSetupStrings.TickRateDescription,
KeyboardStep = 1,
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
{
Default = 1,
@@ -119,6 +121,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup
{
Caption = "Stack Leniency",
HintText = "In play mode, osu! automatically stacks notes which occur at the same location. Increasing this value means it is more likely to snap notes of further time-distance.",
KeyboardStep = 0.1f,
Current = new BindableFloat(Beatmap.StackLeniency)
{
Default = 0.7f,
+1 -1
View File
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
var spinner = (DrawableSpinner)drawable;
spinner.RotationTracker.Tracking = true;
spinner.RotationTracker.Tracking = spinner.RotationTracker.IsSpinnableTime;
// early-return if we were paused to avoid division-by-zero in the subsequent calculations.
if (Precision.AlmostEquals(spinner.Clock.Rate, 0))
@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -149,5 +150,24 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected float CalculateDrawableRelativePosition(Drawable drawable) => (drawable.ScreenSpaceDrawQuad.Centre.X - parentScreenSpaceRectangle.X) / parentScreenSpaceRectangle.Width;
protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(HitObject, judgement);
protected void ApplyRepeatFadeIn(Drawable target, double fadeTime)
{
DrawableSlider slider = (DrawableSlider)ParentHitObject;
int repeatIndex = ((SliderEndCircle)HitObject).RepeatIndex;
Debug.Assert(slider != null);
// When snaking in is enabled, the first end circle needs to be delayed until the snaking completes.
bool delayFadeIn = slider.SliderBody?.SnakingIn.Value == true && repeatIndex == 0;
if (repeatIndex > 0)
fadeTime = Math.Min(slider.HitObject.SpanDuration, fadeTime);
target
.FadeOut()
.Delay(delayFadeIn ? (slider.HitObject.TimePreempt) / 3 : 0)
.FadeIn(fadeTime);
}
}
}
@@ -27,8 +27,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
private double animDuration;
public SkinnableDrawable CirclePiece { get; private set; }
public SkinnableDrawable Arrow { get; private set; }
@@ -87,21 +85,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected override void UpdateInitialTransforms()
{
// When snaking in is enabled, the first end circle needs to be delayed until the snaking completes.
bool delayFadeIn = DrawableSlider.SliderBody?.SnakingIn.Value == true && HitObject.RepeatIndex == 0;
base.UpdateInitialTransforms();
animDuration = Math.Min(300, HitObject.SpanDuration);
this
.FadeOut()
.Delay(delayFadeIn ? (Slider?.TimePreempt ?? 0) / 3 : 0)
.FadeIn(HitObject.RepeatIndex == 0 ? HitObject.TimeFadeIn : animDuration);
ApplyRepeatFadeIn(CirclePiece, HitObject.TimeFadeIn);
ApplyRepeatFadeIn(Arrow, 150);
}
protected override void UpdateHitStateTransforms(ArmedState state)
{
base.UpdateHitStateTransforms(state);
double animDuration = Math.Min(300, HitObject.SpanDuration);
switch (state)
{
case ArmedState.Idle:
@@ -181,10 +176,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
// More or less matches stable (see https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameplayElements/HitObjects/Osu/HitCircleOsu.cs#L336-L338)
AccentColour.Value = Color4.White;
Alpha = Interpolation.ValueAt(Time.Current, 1f, 0f, HitStateUpdateTime, HitStateUpdateTime + 700);
Arrow.Alpha = 0;
}
Arrow.Alpha = hit ? 0 : 1;
LifetimeEnd = HitStateUpdateTime + 700;
}
@@ -86,13 +86,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.UpdateInitialTransforms();
// When snaking in is enabled, the first end circle needs to be delayed until the snaking completes.
bool delayFadeIn = DrawableSlider.SliderBody?.SnakingIn.Value == true && HitObject.RepeatIndex == 0;
CirclePiece
.FadeOut()
.Delay(delayFadeIn ? (Slider?.TimePreempt ?? 0) / 3 : 0)
.FadeIn(HitObject.TimeFadeIn);
ApplyRepeatFadeIn(CirclePiece, HitObject.TimeFadeIn);
}
protected override void UpdateHitStateTransforms(ArmedState state)
@@ -277,13 +277,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
base.Update();
if (HandleUserInput)
{
bool isValidSpinningTime = Time.Current >= HitObject.StartTime && Time.Current <= HitObject.EndTime;
RotationTracker.Tracking = !Result.HasResult
&& correctButtonPressed()
&& isValidSpinningTime;
}
RotationTracker.Tracking = RotationTracker.IsSpinnableTime && !Result.HasResult && correctButtonPressed();
if (spinningSample != null && spinnerFrequencyModulate)
spinningSample.Frequency.Value = spinning_sample_modulated_base_frequency + Progress;
@@ -85,9 +85,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
Scale = new Vector2(Interpolation.ValueAt(Time.Current, 1, 1.5f, drawableRepeat.HitStateUpdateTime, drawableRepeat.HitStateUpdateTime + animDuration, Easing.Out));
// When hit, don't animate further. This avoids a scale being applied on a scale and looking very weird.
return;
}
else
Scale = Vector2.One;
Scale = Vector2.One;
const float move_distance = -12;
const float scale_amount = 1.3f;
@@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
/// <summary>
/// Whether currently in the correct time range to allow spinning.
/// </summary>
private bool isSpinnableTime => drawableSpinner.HitObject.StartTime <= Time.Current && drawableSpinner.HitObject.EndTime > Time.Current;
public bool IsSpinnableTime => drawableSpinner.HitObject.StartTime <= Time.Current && drawableSpinner.HitObject.EndTime > Time.Current;
protected override bool OnMouseMove(MouseMoveEvent e)
{
@@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
lastAngle = thisAngle;
}
IsSpinning.Value = isSpinnableTime && Math.Abs(currentRotation - Rotation) > 10f;
IsSpinning.Value = IsSpinnableTime && Math.Abs(currentRotation - Rotation) > 10f;
Rotation = (float)Interpolation.Damp(Rotation, currentRotation, 0.99, Math.Abs(Time.Elapsed));
}
@@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
/// <param name="delta">The delta angle.</param>
public void AddRotation(float delta)
{
if (!isSpinnableTime)
if (!IsSpinnableTime)
return;
if (!rotationTransferred)
@@ -0,0 +1,102 @@
// 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.ControlPoints;
using osu.Game.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Replays;
using osu.Game.Scoring;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests
{
[Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")]
public partial class TestSceneLegacyReplayPlayback : LegacyReplayPlaybackTestScene
{
protected override string? ExportLocation => null;
protected override Ruleset CreateRuleset() => new TaikoRuleset();
private static readonly object[][] test_cases =
{
// With respect to notation,
// square brackets `[]` represent *closed* or *inclusive* bounds,
// while round brackets `()` represent *open* or *exclusive* bounds.
// OD = 5 test cases.
// GREAT hit window is (-35ms, 35ms)
// OK hit window is (-80ms, 80ms)
new object[] { 5f, -33d, HitResult.Great },
new object[] { 5f, -34d, HitResult.Great },
new object[] { 5f, -35d, HitResult.Ok },
new object[] { 5f, -36d, HitResult.Ok },
new object[] { 5f, -78d, HitResult.Ok },
new object[] { 5f, -79d, HitResult.Ok },
new object[] { 5f, -80d, HitResult.Miss },
new object[] { 5f, -81d, HitResult.Miss },
// OD = 7.8 test cases.
// GREAT hit window is (-26ms, 26ms)
// OK hit window is (-63ms, 63ms)
new object[] { 7.8f, -24d, HitResult.Great },
new object[] { 7.8f, -25d, HitResult.Great },
new object[] { 7.8f, -26d, HitResult.Ok },
new object[] { 7.8f, -27d, HitResult.Ok },
new object[] { 7.8f, -61d, HitResult.Ok },
new object[] { 7.8f, -62d, HitResult.Ok },
new object[] { 7.8f, -63d, HitResult.Miss },
new object[] { 7.8f, -64d, HitResult.Miss },
};
[TestCaseSource(nameof(test_cases))]
public void TestHitWindowTreatment(float overallDifficulty, double hitOffset, HitResult expectedResult)
{
const double hit_time = 100;
var cpi = new ControlPointInfo();
cpi.Add(0, new TimingControlPoint { BeatLength = 1000 });
var beatmap = new TaikoBeatmap
{
HitObjects =
{
new Hit
{
StartTime = hit_time,
Type = HitType.Centre,
}
},
Difficulty = new BeatmapDifficulty { OverallDifficulty = overallDifficulty },
BeatmapInfo =
{
Ruleset = new TaikoRuleset().RulesetInfo,
},
ControlPointInfo = cpi,
};
var replay = new Replay
{
Frames =
{
new TaikoReplayFrame(0),
new TaikoReplayFrame(hit_time + hitOffset, TaikoAction.LeftCentre),
new TaikoReplayFrame(hit_time + hitOffset + 20),
}
};
var score = new Score
{
Replay = replay,
ScoreInfo = new ScoreInfo
{
Ruleset = CreateRuleset().RulesetInfo,
}
};
RunTest($@"single hit @ OD{overallDifficulty}", beatmap, $@"{hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
}
}
}
@@ -0,0 +1,96 @@
// 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.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Replays;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests
{
[Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")]
public partial class TestSceneReplayStability : ReplayStabilityTestScene
{
private static readonly object[][] test_cases =
{
// With respect to notation,
// square brackets `[]` represent *closed* or *inclusive* bounds,
// while round brackets `()` represent *open* or *exclusive* bounds.
// OD = 5 test cases.
// GREAT hit window is [-35ms, 35ms]
// OK hit window is [-80ms, 80ms]
// MISS hit window is [-95ms, 95ms]
new object[] { 5f, -34d, HitResult.Great },
new object[] { 5f, -34.2d, HitResult.Great },
new object[] { 5f, -34.7d, HitResult.Great },
new object[] { 5f, -35d, HitResult.Great },
new object[] { 5f, -35.2d, HitResult.Ok },
new object[] { 5f, -35.8d, HitResult.Ok },
new object[] { 5f, -36d, HitResult.Ok },
new object[] { 5f, -79d, HitResult.Ok },
new object[] { 5f, -79.3d, HitResult.Ok },
new object[] { 5f, -79.7d, HitResult.Ok },
new object[] { 5f, -80d, HitResult.Ok },
new object[] { 5f, -80.2d, HitResult.Miss },
new object[] { 5f, -80.8d, HitResult.Miss },
new object[] { 5f, -81d, HitResult.Miss },
// OD = 7.8 test cases.
// GREAT hit window is [-26.6ms, 26.6ms]
// OK hit window is [-63.2ms, 63.2ms]
// MISS hit window is [-81.0ms, 81.0ms]
new object[] { 7.8f, -26d, HitResult.Great },
new object[] { 7.8f, -26.4d, HitResult.Great },
new object[] { 7.8f, -26.59d, HitResult.Great },
new object[] { 7.8f, -26.8d, HitResult.Ok },
new object[] { 7.8f, -27d, HitResult.Ok },
new object[] { 7.8f, -27.1d, HitResult.Ok },
new object[] { 7.8f, -63d, HitResult.Ok },
new object[] { 7.8f, -63.18d, HitResult.Ok },
new object[] { 7.8f, -63.4d, HitResult.Ok },
new object[] { 7.8f, -63.7d, HitResult.Miss },
new object[] { 7.8f, -64d, HitResult.Miss },
new object[] { 7.8f, -64.2d, HitResult.Miss },
};
[TestCaseSource(nameof(test_cases))]
public void TestHitWindowStability(float overallDifficulty, double hitOffset, HitResult expectedResult)
{
const double hit_time = 100;
var beatmap = new TaikoBeatmap
{
HitObjects =
{
new Hit
{
StartTime = hit_time,
Type = HitType.Centre,
}
},
Difficulty = new BeatmapDifficulty { OverallDifficulty = overallDifficulty },
BeatmapInfo =
{
Ruleset = new TaikoRuleset().RulesetInfo,
},
};
var replay = new Replay
{
Frames =
{
new TaikoReplayFrame(0),
new TaikoReplayFrame(hit_time + hitOffset, TaikoAction.LeftCentre),
new TaikoReplayFrame(hit_time + hitOffset + 20),
}
};
RunTest(beatmap, replay, [expectedResult]);
}
}
}
@@ -1,6 +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.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
@@ -15,26 +16,30 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
int hits = HitObjects.Count(s => s is Hit);
int drumRolls = HitObjects.Count(s => s is DrumRoll);
int swells = HitObjects.Count(s => s is Swell);
int sum = Math.Max(1, hits + drumRolls);
return new[]
{
new BeatmapStatistic
{
Name = @"Hit Count",
Name = @"Hits",
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
Content = hits.ToString(),
BarDisplayLength = hits / (float)sum,
},
new BeatmapStatistic
{
Name = @"Drumroll Count",
Name = @"Drumrolls",
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
Content = drumRolls.ToString(),
BarDisplayLength = drumRolls / (float)sum,
},
new BeatmapStatistic
{
Name = @"Swell Count",
Name = @"Swells",
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners),
Content = swells.ToString(),
BarDisplayLength = Math.Min(swells / 10f, 1),
}
};
}
@@ -60,6 +60,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Setup
{
Caption = EditorSetupStrings.BaseVelocity,
HintText = EditorSetupStrings.BaseVelocityDescription,
KeyboardStep = 0.1f,
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
{
Default = 1.4,
@@ -74,6 +75,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Setup
{
Caption = EditorSetupStrings.TickRate,
HintText = EditorSetupStrings.TickRateDescription,
KeyboardStep = 1,
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
{
Default = 1,
@@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Shapes;
@@ -112,5 +113,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
.FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine);
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableHitObject.IsNotNull())
drawableHitObject.ApplyCustomUpdateState -= updateStateTransforms;
}
}
}
@@ -4,6 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
@@ -202,5 +203,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
.Then()
.FadeEdgeEffectTo(edge_alpha_kiai, duration, Easing.OutQuint);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableHitObject.IsNotNull())
drawableHitObject.ApplyCustomUpdateState -= updateStateTransforms;
}
}
}
@@ -17,12 +17,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{
internal partial class LegacyKiaiGlow : BeatSyncedContainer
{
private bool isKiaiActive;
[Resolved]
private HealthProcessor? healthProcessor { get; set; }
private bool isKiaiActive;
private Sprite sprite = null!;
[BackgroundDependencyLoader(true)]
private void load(ISkinSource skin, HealthProcessor? healthProcessor)
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
Child = sprite = new Sprite
{
@@ -33,6 +35,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
Scale = new Vector2(TaikoLegacyHitTarget.SCALE),
Colour = new Colour4(255, 228, 0, 255),
};
}
protected override void LoadComplete()
{
base.LoadComplete();
if (healthProcessor != null)
healthProcessor.NewJudgement += onNewJudgement;
@@ -61,5 +68,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
sprite.ScaleTo(TaikoLegacyHitTarget.SCALE + 0.15f).Then()
.ScaleTo(TaikoLegacyHitTarget.SCALE, 80, Easing.OutQuad);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (healthProcessor != null)
healthProcessor.NewJudgement -= onNewJudgement;
}
}
}
@@ -28,6 +28,7 @@
<ProjectReference Include="..\osu.Game\osu.Game.csproj" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Bogus" Version="35.6.2" />
<PackageReference Include="DeepEqual" Version="2.0.0" />
<PackageReference Include="Moq" Version="4.17.2" />
</ItemGroup>
@@ -29,6 +29,7 @@
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Bogus" Version="35.6.2" />
<PackageReference Include="DeepEqual" Version="2.0.0" />
<PackageReference Include="Moq" Version="4.17.2" />
</ItemGroup>
@@ -135,6 +135,24 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
[Test]
public void TestNoopFadeTransformIsIgnoredForLifetime()
{
var decoder = new LegacyStoryboardDecoder();
using (var resStream = TestResources.OpenResource("noop-fade-transform-is-ignored-for-lifetime.osb"))
using (var stream = new LineBufferedReader(resStream))
{
var storyboard = decoder.Decode(stream);
StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3);
Assert.AreEqual(2, background.Elements.Count);
Assert.AreEqual(1500, background.Elements[0].StartTime);
Assert.AreEqual(1500, background.Elements[1].StartTime);
}
}
[Test]
public void TestOutOfOrderStartTimes()
{
@@ -288,6 +306,29 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
[Test]
public void TestVideoWithCustomFadeIn()
{
var decoder = new LegacyStoryboardDecoder();
using var resStream = TestResources.OpenResource("video-custom-alpha-transform.osb");
using var stream = new LineBufferedReader(resStream);
var storyboard = decoder.Decode(stream);
Assert.Multiple(() =>
{
Assert.That(storyboard.GetLayer(@"Video").Elements, Has.Count.EqualTo(1));
Assert.That(storyboard.GetLayer(@"Video").Elements.Single(), Is.InstanceOf<StoryboardVideo>());
Assert.That(storyboard.GetLayer(@"Video").Elements.Single().StartTime, Is.EqualTo(-5678));
Assert.That(((StoryboardVideo)storyboard.GetLayer(@"Video").Elements.Single()).Commands.Alpha.Single().StartTime, Is.EqualTo(1500));
Assert.That(((StoryboardVideo)storyboard.GetLayer(@"Video").Elements.Single()).Commands.Alpha.Single().EndTime, Is.EqualTo(1600));
Assert.That(storyboard.EarliestEventTime, Is.Null);
Assert.That(storyboard.LatestEventTime, Is.Null);
});
}
[Test]
public void TestVideoAndBackgroundEventsDoNotAffectStoryboardBounds()
{
@@ -0,0 +1,54 @@
// 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.Extensions;
namespace osu.Game.Tests.Extensions
{
[TestFixture]
public class NumberFormattingExtensionsTest
{
[TestCase(-1, false, 0, ExpectedResult = "-1")]
[TestCase(0, false, 0, ExpectedResult = "0")]
[TestCase(1, false, 0, ExpectedResult = "1")]
[TestCase(500, false, 10, ExpectedResult = "500")]
[TestCase(-1, true, 0, ExpectedResult = "-1%")]
[TestCase(0, true, 0, ExpectedResult = "0%")]
[TestCase(1, true, 0, ExpectedResult = "1%")]
[TestCase(50, true, 0, ExpectedResult = "50%")]
public string TestInteger(int input, bool percent, int decimalDigits)
{
return input.ToStandardFormattedString(decimalDigits, percent);
}
[TestCase(-1, false, 0, ExpectedResult = "-1")]
[TestCase(-1e-6, false, 0, ExpectedResult = "0")]
[TestCase(-1e-6, false, 6, ExpectedResult = "-0.000001")]
[TestCase(0, false, 10, ExpectedResult = "0")]
[TestCase(0, false, 0, ExpectedResult = "0")]
[TestCase(double.NegativeZero, false, 0, ExpectedResult = "0")]
[TestCase(1e-6, false, 0, ExpectedResult = "0")]
[TestCase(1e-6, false, 6, ExpectedResult = "0.000001")]
[TestCase(1, false, 0, ExpectedResult = "1")]
[TestCase(1.528, false, 2, ExpectedResult = "1.53")]
[TestCase(500, false, 10, ExpectedResult = "500")]
[TestCase(-0.1, true, 0, ExpectedResult = "-10%")]
[TestCase(0, true, 0, ExpectedResult = "0%")]
[TestCase(0.4, true, 0, ExpectedResult = "40%")]
[TestCase(0.48333, true, 2, ExpectedResult = "48%")]
[TestCase(0.48333, true, 4, ExpectedResult = "48.33%")]
[TestCase(1, true, 0, ExpectedResult = "100%")]
public string TestDouble(double input, bool percent, int decimalDigits)
{
return input.ToStandardFormattedString(decimalDigits, percent);
}
[Test]
[SetCulture("fr-FR")]
public void TestCultureInsensitivity()
{
Assert.That(0.4.ToStandardFormattedString(maxDecimalDigits: 2, asPercentage: true), Is.EqualTo("40%"));
}
}
}
@@ -0,0 +1,73 @@
// 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 Bogus;
using MessagePack;
using NUnit.Framework;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
namespace osu.Game.Tests.OnlinePlay
{
[TestFixture]
public class MultiplayerPlaylistItemTest
{
[SetUp]
public void Setup()
{
Randomizer.Seed = new Random(1337);
}
[Test]
public void TestCloneMultiplayerPlaylistItem()
{
var faker = new Faker<MultiplayerPlaylistItem>()
.StrictMode(true)
.RuleFor(o => o.ID, f => f.Random.Long())
.RuleFor(o => o.OwnerID, f => f.Random.Int())
.RuleFor(o => o.BeatmapID, f => f.Random.Int())
.RuleFor(o => o.BeatmapChecksum, f => f.Random.Hash())
.RuleFor(o => o.RulesetID, f => f.Random.Int())
.RuleFor(o => o.RequiredMods, f => f.Make(5, _ => new APIMod { Acronym = f.Random.String2(3) }))
.RuleFor(o => o.AllowedMods, f => f.Make(5, _ => new APIMod { Acronym = f.Random.String2(3) }))
.RuleFor(o => o.Expired, f => f.Random.Bool())
.RuleFor(o => o.PlaylistOrder, f => f.Random.UShort())
.RuleFor(o => o.PlayedAt, f => f.Date.RecentOffset())
.RuleFor(o => o.StarRating, f => f.Random.Double())
.RuleFor(o => o.Freestyle, f => f.Random.Bool());
for (int i = 0; i < 100; i++)
{
MultiplayerPlaylistItem item = faker.Generate();
Assert.That(MessagePackSerializer.SerializeToJson(item.Clone()), Is.EqualTo(MessagePackSerializer.SerializeToJson(item)));
}
}
[Test]
public void TestConstructFromAPIModel()
{
var faker = new Faker<MultiplayerPlaylistItem>()
.StrictMode(true)
.RuleFor(o => o.ID, f => f.Random.Long())
.RuleFor(o => o.OwnerID, f => f.Random.Int())
.RuleFor(o => o.BeatmapID, f => f.Random.Int())
.RuleFor(o => o.BeatmapChecksum, f => f.Random.Hash())
.RuleFor(o => o.RulesetID, f => f.Random.Int())
.RuleFor(o => o.RequiredMods, f => f.Make(5, _ => new APIMod { Acronym = f.Random.String2(3) }))
.RuleFor(o => o.AllowedMods, f => f.Make(5, _ => new APIMod { Acronym = f.Random.String2(3) }))
.RuleFor(o => o.Expired, f => f.Random.Bool())
.RuleFor(o => o.PlaylistOrder, f => f.Random.UShort())
.RuleFor(o => o.PlayedAt, f => f.Date.RecentOffset())
.RuleFor(o => o.StarRating, f => f.Random.Double())
.RuleFor(o => o.Freestyle, f => f.Random.Bool());
for (int i = 0; i < 100; i++)
{
MultiplayerPlaylistItem initialItem = faker.Generate();
MultiplayerPlaylistItem copiedItem = new MultiplayerPlaylistItem(new PlaylistItem(initialItem));
Assert.That(MessagePackSerializer.SerializeToJson(copiedItem), Is.EqualTo(MessagePackSerializer.SerializeToJson(initialItem)));
}
}
}
}
@@ -0,0 +1,8 @@
[Events]
//Storyboard Layer 0 (Background)
Sprite,Background,TopCentre,"img.jpg",320,240
F,0,1000,1000,0,0 // should be ignored
F,0,1500,1600,0,1
Sprite,Background,TopCentre,"img.jpg",320,240
F,0,1000,1000,0,0 // should be ignored
F,0,1500,1600,1,1
@@ -0,0 +1,5 @@
osu file format v14
[Events]
Video,-5678,"Video.avi",0,0
F,0,1500,1600,0,1
@@ -215,6 +215,35 @@ namespace osu.Game.Tests.Scores.IO
}
}
[Test]
public void TestScoreWithInvalidModCombinationsWillNotImport()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
var osu = LoadOsuIntoHost(host, true);
var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely();
var toImport = new ScoreInfo
{
User = new APIUser { Username = "Test user" },
BeatmapInfo = beatmap.Beatmaps.First(),
Ruleset = new OsuRuleset().RulesetInfo,
ClientVersion = "12345",
Mods = new Mod[] { new OsuModHalfTime(), new OsuModDoubleTime() },
};
Assert.Throws<InvalidOperationException>(() => LoadScoreIntoOsu(osu, toImport));
}
finally
{
host.Exit();
}
}
}
[Test]
public void TestImportStatistics()
{
@@ -91,6 +91,6 @@ namespace osu.Game.Tests.Visual.Beatmaps
}
private void assertCorrectIcon(bool favourited) => AddAssert("icon correct",
() => this.ChildrenOfType<SpriteIcon>().Single().Icon.Equals(favourited ? FontAwesome.Solid.Heart : FontAwesome.Regular.Heart));
() => this.ChildrenOfType<SpriteIcon>().First().Icon.Equals(favourited ? FontAwesome.Solid.Heart : FontAwesome.Regular.Heart));
}
}
@@ -19,6 +19,8 @@ namespace osu.Game.Tests.Visual.Beatmaps
{
public partial class TestSceneBeatmapSetOnlineStatusPill : ThemeComparisonTestScene
{
private bool showUnknownStatus;
protected override Drawable CreateContent() => new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
@@ -26,12 +28,20 @@ namespace osu.Game.Tests.Visual.Beatmaps
Origin = Anchor.Centre,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 10),
ChildrenEnumerable = Enum.GetValues(typeof(BeatmapOnlineStatus)).Cast<BeatmapOnlineStatus>().Select(status => new BeatmapSetOnlineStatusPill
ChildrenEnumerable = Enum.GetValues(typeof(BeatmapOnlineStatus)).Cast<BeatmapOnlineStatus>().Select(status => new Container
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Status = status
RelativeSizeAxes = Axes.X,
Height = 20,
Children = new Drawable[]
{
new BeatmapSetOnlineStatusPill
{
ShowUnknownStatus = showUnknownStatus,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Status = status
}
}
})
};
@@ -48,6 +58,12 @@ namespace osu.Game.Tests.Visual.Beatmaps
pill.Width = 90;
}));
AddStep("toggle show unknown", () =>
{
showUnknownStatus = !showUnknownStatus;
CreateThemedContent(OverlayColourScheme.Red);
});
AddStep("unset fixed width", () => statusPills.ForEach(pill => pill.AutoSizeAxes = Axes.Both));
}
@@ -65,11 +81,6 @@ namespace osu.Game.Tests.Visual.Beatmaps
pill.Status = BeatmapOnlineStatus.LocallyModified;
break;
// skip none
case BeatmapOnlineStatus.LocallyModified:
pill.Status = BeatmapOnlineStatus.Graveyard;
break;
default:
pill.Status = (pill.Status + 1);
break;
@@ -15,7 +15,7 @@ using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.SelectV2.Leaderboards;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual.Metadata;
using osu.Game.Tests.Visual.OnlinePlay;
@@ -43,7 +43,6 @@ namespace osu.Game.Tests.Visual.DailyChallenge
{
var room = new Room
{
RoomID = 1234,
Name = "Daily Challenge: June 4, 2024",
Playlist =
[
@@ -66,7 +65,6 @@ namespace osu.Game.Tests.Visual.DailyChallenge
{
var room = new Room
{
RoomID = 1234,
Name = "Daily Challenge: June 4, 2024",
Playlist =
[
@@ -87,7 +85,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge
AddStep("force transforms to finish", () => FinishTransforms(true));
AddStep("right click second score", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<LeaderboardScoreV2>().ElementAt(1));
InputManager.MoveMouseTo(this.ChildrenOfType<BeatmapLeaderboardScore>().ElementAt(1));
InputManager.Click(MouseButton.Right);
});
AddAssert("use these mods not present",
@@ -99,7 +97,6 @@ namespace osu.Game.Tests.Visual.DailyChallenge
{
var room = new Room
{
RoomID = 1234,
Name = "Daily Challenge: June 4, 2024",
Playlist =
[
@@ -114,7 +111,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge
};
AddStep("add room", () => API.Perform(new CreateRoomRequest(room)));
AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1234 });
AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = room.RoomID!.Value });
Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!;
AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
@@ -128,7 +125,6 @@ namespace osu.Game.Tests.Visual.DailyChallenge
{
var room = new Room
{
RoomID = 1234,
Name = "Daily Challenge: June 4, 2024",
Playlist =
[
@@ -143,7 +139,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge
};
AddStep("add room", () => API.Perform(new CreateRoomRequest(room)));
AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1234 });
AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = room.RoomID!.Value });
Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!;
AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
@@ -44,17 +44,17 @@ namespace osu.Game.Tests.Visual.DailyChallenge
[Test]
public void TestDailyChallenge()
{
startChallenge(1234);
startChallenge();
AddStep("push screen", () => LoadScreen(new DailyChallengeIntro(room)));
}
[Test]
public void TestPlayIntroOnceFlag()
{
startChallenge(1234);
startChallenge();
AddStep("set intro played flag", () => Dependencies.Get<SessionStatics>().SetValue(Static.DailyChallengeIntroPlayed, true));
startChallenge(1235);
startChallenge();
AddAssert("intro played flag reset", () => Dependencies.Get<SessionStatics>().Get<bool>(Static.DailyChallengeIntroPlayed), () => Is.False);
@@ -62,13 +62,12 @@ namespace osu.Game.Tests.Visual.DailyChallenge
AddUntilStep("intro played flag set", () => Dependencies.Get<SessionStatics>().Get<bool>(Static.DailyChallengeIntroPlayed), () => Is.True);
}
private void startChallenge(int roomId)
private void startChallenge()
{
AddStep("add room", () =>
{
API.Perform(new CreateRoomRequest(room = new Room
{
RoomID = roomId,
Name = "Daily Challenge: June 4, 2024",
Playlist =
[
@@ -83,7 +82,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge
Category = RoomCategory.DailyChallenge
}));
});
AddStep("signal client", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { RoomID = roomId }));
AddStep("signal client", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { RoomID = room.RoomID!.Value }));
}
}
}
@@ -24,12 +24,7 @@ namespace osu.Game.Tests.Visual.Editing
PoolableSkinnableSample[] loopingSamples = null;
PoolableSkinnableSample[] onceOffSamples = null;
AddStep("get first slider", () =>
{
slider = Editor.ChildrenOfType<DrawableSlider>().OrderBy(s => s.HitObject.StartTime).First();
onceOffSamples = slider.ChildrenOfType<PoolableSkinnableSample>().Where(s => !s.Looping).ToArray();
loopingSamples = slider.ChildrenOfType<PoolableSkinnableSample>().Where(s => s.Looping).ToArray();
});
AddStep("get first slider", () => slider = Editor.ChildrenOfType<DrawableSlider>().OrderBy(s => s.HitObject.StartTime).First());
AddStep("start playback", () => EditorClock.Start());
@@ -38,6 +33,9 @@ namespace osu.Game.Tests.Visual.Editing
if (!slider.Tracking.Value)
return false;
onceOffSamples = slider.ChildrenOfType<PoolableSkinnableSample>().Where(s => !s.Looping).ToArray();
loopingSamples = slider.ChildrenOfType<PoolableSkinnableSample>().Where(s => s.Looping).ToArray();
if (!loopingSamples.Any(s => s.Playing))
return false;
@@ -65,10 +65,10 @@ namespace osu.Game.Tests.Visual.Editing
InputManager.Keys(PlatformAction.Paste);
});
assertArtistMetadata("Example Artist");
assertArtistMetadata("Example ArtistExample Artist");
// It's important values are committed immediately on focus loss so the editor exit sequence detects them.
AddAssert("value immediately changed on focus loss", () =>
AddAssert("value still changed after focus loss", () =>
{
((IFocusManager)InputManager).TriggerFocusContention(metadataSection);
return editorBeatmap.Metadata.Artist;
@@ -104,7 +104,7 @@ namespace osu.Game.Tests.Visual.Editing
InputManager.Keys(PlatformAction.Paste);
});
assertArtistMetadata("Example Artist");
assertArtistMetadata("Example ArtistExample Artist");
AddStep("commit", () => InputManager.Key(Key.Enter));
@@ -50,21 +50,17 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddStep("Set short reference score", () =>
{
// 50 events total. one of them (head circle) being timed / having hitwindows, rest having no hitwindows
List<HitEvent> hitEvents =
[
// 10 events total. one of them (head circle) being timed / having hitwindows, rest having no hitwindows
new HitEvent(30, 1, HitResult.LargeTickHit, new SliderHeadCircle { ClassicSliderBehaviour = true }, null, null),
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
];
for (int i = 0; i < 49; i++)
{
hitEvents.Add(new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null));
}
foreach (var ev in hitEvents)
ev.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
@@ -1,12 +1,12 @@
// 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.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Extensions.PolygonExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -16,6 +16,7 @@ using osu.Framework.Utils;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Select.Leaderboards;
using osuTK;
namespace osu.Game.Tests.Visual.Gameplay
@@ -23,7 +24,10 @@ namespace osu.Game.Tests.Visual.Gameplay
[TestFixture]
public partial class TestSceneGameplayLeaderboard : OsuTestScene
{
private TestGameplayLeaderboard leaderboard;
private TestDrawableGameplayLeaderboard leaderboard = null!;
[Cached(typeof(IGameplayLeaderboardProvider))]
private TestGameplayLeaderboardProvider leaderboardProvider = new TestGameplayLeaderboardProvider();
private readonly BindableLong playerScore = new BindableLong();
@@ -31,7 +35,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddStep("toggle expanded", () =>
{
if (leaderboard != null)
if (leaderboard.IsNotNull())
leaderboard.Expanded.Value = !leaderboard.Expanded.Value;
});
@@ -57,10 +61,10 @@ namespace osu.Game.Tests.Visual.Gameplay
// has caused layout to not work in the past.
AddUntilStep("wait for fill flow layout",
() => leaderboard.ChildrenOfType<FillFlowContainer<GameplayLeaderboardScore>>().First().ScreenSpaceDrawQuad.Intersects(leaderboard.ScreenSpaceDrawQuad));
() => leaderboard.ChildrenOfType<FillFlowContainer<DrawableGameplayLeaderboardScore>>().First().ScreenSpaceDrawQuad.Intersects(leaderboard.ScreenSpaceDrawQuad));
AddUntilStep("wait for some scores not masked away",
() => leaderboard.ChildrenOfType<GameplayLeaderboardScore>().Any(s => leaderboard.ScreenSpaceDrawQuad.Contains(s.ScreenSpaceDrawQuad.Centre)));
() => leaderboard.ChildrenOfType<DrawableGameplayLeaderboardScore>().Any(s => leaderboard.ScreenSpaceDrawQuad.Contains(s.ScreenSpaceDrawQuad.Centre)));
AddUntilStep("wait for tracked score fully visible", () => leaderboard.ScreenSpaceDrawQuad.Intersects(leaderboard.TrackedScore!.ScreenSpaceDrawQuad));
@@ -139,7 +143,7 @@ namespace osu.Game.Tests.Visual.Gameplay
checkHeight(8);
void checkHeight(int panelCount)
=> AddAssert($"leaderboard height is {panelCount} panels high", () => leaderboard.DrawHeight == (GameplayLeaderboardScore.PANEL_HEIGHT + leaderboard.Spacing) * panelCount);
=> AddAssert($"leaderboard height is {panelCount} panels high", () => leaderboard.DrawHeight == (DrawableGameplayLeaderboardScore.PANEL_HEIGHT + leaderboard.Spacing) * panelCount);
}
[Test]
@@ -179,6 +183,30 @@ namespace osu.Game.Tests.Visual.Gameplay
() => Does.Contain("#FF549A"));
}
[Test]
public void TestTrackedScorePosition([Values] bool partial)
{
createLeaderboard(partial);
AddStep("add many scores in one go", () =>
{
for (int i = 0; i < 49; i++)
createRandomScore(new APIUser { Username = $"Player {i + 1}" });
// Add player at end to force an animation down the whole list.
playerScore.Value = 0;
createLeaderboardScore(playerScore, new APIUser { Username = "You", Id = 3 }, true);
});
if (partial)
AddUntilStep("tracked player has null position", () => leaderboard.TrackedScore?.ScorePosition, () => Is.Null);
else
AddUntilStep("tracked player is #50", () => leaderboard.TrackedScore?.ScorePosition, () => Is.EqualTo(50));
AddStep("move tracked player to top", () => leaderboard.TrackedScore!.TotalScore.Value = 8_000_000);
AddUntilStep("all players have non-null position", () => leaderboard.AllScores.Select(s => s.ScorePosition), () => Does.Not.Contain(null));
}
private void addLocalPlayer()
{
AddStep("add local player", () =>
@@ -188,11 +216,13 @@ namespace osu.Game.Tests.Visual.Gameplay
});
}
private void createLeaderboard()
private void createLeaderboard(bool partial = false)
{
AddStep("create leaderboard", () =>
{
Child = leaderboard = new TestGameplayLeaderboard
leaderboardProvider.Scores.Clear();
leaderboardProvider.IsPartial = partial;
Child = leaderboard = new TestDrawableGameplayLeaderboard
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -205,11 +235,11 @@ namespace osu.Game.Tests.Visual.Gameplay
private void createLeaderboardScore(BindableLong score, APIUser user, bool isTracked = false)
{
var leaderboardScore = leaderboard.Add(user, isTracked);
leaderboardScore.TotalScore.BindTo(score);
var leaderboardScore = new GameplayLeaderboardScore(user, isTracked, score);
leaderboardProvider.Scores.Add(leaderboardScore);
}
private partial class TestGameplayLeaderboard : GameplayLeaderboard
private partial class TestDrawableGameplayLeaderboard : DrawableGameplayLeaderboard
{
public float Spacing => Flow.Spacing.Y;
@@ -220,8 +250,17 @@ namespace osu.Game.Tests.Visual.Gameplay
return scoreItem != null && scoreItem.ScorePosition == expectedPosition;
}
public IEnumerable<GameplayLeaderboardScore> GetAllScoresForUsername(string username)
public IEnumerable<DrawableGameplayLeaderboardScore> GetAllScoresForUsername(string username)
=> Flow.Where(i => i.User?.Username == username);
public IEnumerable<DrawableGameplayLeaderboardScore> AllScores => Flow;
}
private class TestGameplayLeaderboardProvider : IGameplayLeaderboardProvider
{
IBindableList<GameplayLeaderboardScore> IGameplayLeaderboardProvider.Scores => Scores;
public BindableList<GameplayLeaderboardScore> Scores { get; } = new BindableList<GameplayLeaderboardScore>();
public bool IsPartial { get; set; }
}
}
}
@@ -15,6 +15,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.SkinEditor;
@@ -458,6 +459,62 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType<LegacyDefaultComboCounter>().Count() == 1);
}
[Test]
public void TestAnchorRadioButtonBehavior()
{
ISerialisableDrawable? selectedComponent = null;
AddStep("Select first component", () =>
{
var blueprint = skinEditor.ChildrenOfType<SkinBlueprint>().First();
skinEditor.SelectedComponents.Clear();
skinEditor.SelectedComponents.Add(blueprint.Item);
selectedComponent = blueprint.Item;
});
AddStep("Right-click to open context menu", () =>
{
if (selectedComponent != null)
InputManager.MoveMouseTo(((Drawable)selectedComponent).ScreenSpaceDrawQuad.Centre);
InputManager.Click(MouseButton.Right);
});
AddStep("Click on Anchor menu", () =>
{
InputManager.MoveMouseTo(getMenuItemByText("Anchor"));
InputManager.Click(MouseButton.Left);
});
AddStep("Right-click TopLeft anchor", () =>
{
InputManager.MoveMouseTo(getMenuItemByText("TopLeft"));
InputManager.Click(MouseButton.Right);
});
AddAssert("TopLeft item checked", () => (getMenuItemByText("TopLeft").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.True);
AddStep("Right-click Centre anchor", () =>
{
InputManager.MoveMouseTo(getMenuItemByText("Centre"));
InputManager.Click(MouseButton.Right);
});
AddAssert("Centre item checked", () => (getMenuItemByText("Centre").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.True);
AddAssert("TopLeft item unchecked", () => (getMenuItemByText("TopLeft").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.False);
AddStep("Right-click Closest anchor", () =>
{
InputManager.MoveMouseTo(getMenuItemByText("Closest"));
InputManager.Click(MouseButton.Right);
});
AddAssert("Closest item checked", () => (getMenuItemByText("Closest").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.True);
AddAssert("Centre item unchecked", () => (getMenuItemByText("Centre").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.False);
Menu.DrawableMenuItem getMenuItemByText(string text)
=> this.ChildrenOfType<Menu.DrawableMenuItem>().First(m => m.Item.Text.ToString() == text);
}
private Skin importSkinFromArchives(string filename)
{
var imported = skins.Import(new ImportTask(TestResources.OpenResource($@"Archives/{filename}"), filename)).GetResultSafely();
@@ -1,124 +0,0 @@
// 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.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Select;
using osu.Game.Tests.Gameplay;
namespace osu.Game.Tests.Visual.Gameplay
{
public partial class TestSceneSoloGameplayLeaderboard : OsuTestScene
{
[Cached(typeof(ScoreProcessor))]
private readonly ScoreProcessor scoreProcessor = TestGameplayState.Create(new OsuRuleset()).ScoreProcessor;
private readonly BindableList<ScoreInfo> scores = new BindableList<ScoreInfo>();
private readonly Bindable<bool> configVisibility = new Bindable<bool>();
private readonly Bindable<PlayBeatmapDetailArea.TabType> beatmapTabType = new Bindable<PlayBeatmapDetailArea.TabType>();
private SoloGameplayLeaderboard leaderboard = null!;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
config.BindWith(OsuSetting.GameplayLeaderboard, configVisibility);
config.BindWith(OsuSetting.BeatmapDetailTab, beatmapTabType);
}
[SetUpSteps]
public void SetUpSteps()
{
AddStep("clear scores", () => scores.Clear());
AddStep("create component", () =>
{
var trackingUser = new APIUser
{
Username = "local user",
Id = 2,
};
Child = leaderboard = new SoloGameplayLeaderboard(trackingUser)
{
Scores = { BindTarget = scores },
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AlwaysVisible = { Value = false },
Expanded = { Value = true },
};
});
AddStep("add scores", () => scores.AddRange(createSampleScores()));
}
[Test]
public void TestLocalUser()
{
AddSliderStep("score", 0, 1000000, 500000, v => scoreProcessor.TotalScore.Value = v);
AddSliderStep("accuracy", 0f, 1f, 0.5f, v => scoreProcessor.Accuracy.Value = v);
AddSliderStep("combo", 0, 10000, 0, v => scoreProcessor.HighestCombo.Value = v);
AddStep("toggle expanded", () => leaderboard.Expanded.Value = !leaderboard.Expanded.Value);
}
[TestCase(PlayBeatmapDetailArea.TabType.Local, 51)]
[TestCase(PlayBeatmapDetailArea.TabType.Global, null)]
[TestCase(PlayBeatmapDetailArea.TabType.Country, null)]
[TestCase(PlayBeatmapDetailArea.TabType.Friends, null)]
public void TestTrackedScorePosition(PlayBeatmapDetailArea.TabType tabType, int? expectedOverflowIndex)
{
AddStep($"change TabType to {tabType}", () => beatmapTabType.Value = tabType);
AddUntilStep("tracked player is #50", () => leaderboard.TrackedScore?.ScorePosition, () => Is.EqualTo(50));
AddStep("add one more score", () => scores.Add(new ScoreInfo { User = new APIUser { Username = "New player 1" }, TotalScore = RNG.Next(600000, 1000000) }));
AddUntilStep("wait for sort", () => leaderboard.ChildrenOfType<GameplayLeaderboardScore>().First().ScorePosition != null);
if (expectedOverflowIndex == null)
AddUntilStep("tracked player has null position", () => leaderboard.TrackedScore?.ScorePosition, () => Is.Null);
else
AddUntilStep($"tracked player is #{expectedOverflowIndex}", () => leaderboard.TrackedScore?.ScorePosition, () => Is.EqualTo(expectedOverflowIndex));
}
[Test]
public void TestVisibility()
{
AddStep("set config visible true", () => configVisibility.Value = true);
AddUntilStep("leaderboard visible", () => leaderboard.Alpha == 1);
AddStep("set config visible false", () => configVisibility.Value = false);
AddUntilStep("leaderboard not visible", () => leaderboard.Alpha == 0);
AddStep("set always visible", () => leaderboard.AlwaysVisible.Value = true);
AddUntilStep("leaderboard visible", () => leaderboard.Alpha == 1);
AddStep("set config visible true", () => configVisibility.Value = true);
AddAssert("leaderboard still visible", () => leaderboard.Alpha == 1);
}
private static List<ScoreInfo> createSampleScores()
{
return new[]
{
new ScoreInfo { User = new APIUser { Username = @"peppy" }, TotalScore = RNG.Next(500000, 1000000) },
new ScoreInfo { User = new APIUser { Username = @"smoogipoo" }, TotalScore = RNG.Next(500000, 1000000) },
new ScoreInfo { User = new APIUser { Username = @"spaceman_atlas" }, TotalScore = RNG.Next(500000, 1000000) },
new ScoreInfo { User = new APIUser { Username = @"frenzibyte" }, TotalScore = RNG.Next(500000, 1000000) },
new ScoreInfo { User = new APIUser { Username = @"Susko3" }, TotalScore = RNG.Next(500000, 1000000) },
}.Concat(Enumerable.Range(0, 44).Select(i => new ScoreInfo { User = new APIUser { Username = $"User {i + 1}" }, TotalScore = 1000000 - i * 10000 })).ToList();
}
}
}
@@ -2,12 +2,14 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
@@ -48,7 +50,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}
[Test]
public void TestVideoSize()
public void TestVideo()
{
AddStep("load storyboard with only video", () =>
{
@@ -56,6 +58,7 @@ namespace osu.Game.Tests.Visual.Gameplay
loadStoryboard("storyboard_only_video.osu", s => s.Beatmap.WidescreenStoryboard = false);
});
AddAssert("storyboard video present in hierarchy", () => this.ChildrenOfType<DrawableStoryboardVideo>().Any());
AddAssert("storyboard is correct width", () => Precision.AlmostEquals(storyboard?.Width ?? 0f, 480 * 16 / 9f));
}
@@ -185,8 +185,12 @@ namespace osu.Game.Tests.Visual.Menus
AddUntilStep("track changed", () => trackChangeQueue.Count == 1);
AddStep("press previous", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPrev));
AddUntilStep("track changed", () =>
AddUntilStep("new track selected", () =>
trackChangeQueue.Count == 2 && !trackChangeQueue.First().working.BeatmapInfo.Equals(trackChangeQueue.Last().working.BeatmapInfo));
AddStep("press next", () => globalActionContainer.TriggerPressed(GlobalAction.MusicNext));
AddUntilStep("first track selected",
() => trackChangeQueue.Count == 3 && trackChangeQueue.First().working.BeatmapInfo.Equals(trackChangeQueue.Last().working.BeatmapInfo));
}
}
}
@@ -10,6 +10,7 @@ using Moq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Configuration;
@@ -20,6 +21,7 @@ using osu.Game.Online.Spectator;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Select.Leaderboards;
namespace osu.Game.Tests.Visual.Multiplayer
{
@@ -29,11 +31,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
protected readonly BindableList<MultiplayerRoomUser> MultiplayerUsers = new BindableList<MultiplayerRoomUser>();
protected MultiplayerGameplayLeaderboard? Leaderboard { get; private set; }
protected MultiplayerLeaderboardProvider? LeaderboardProvider { get; private set; }
protected DrawableGameplayLeaderboard? Leaderboard { get; private set; }
protected virtual MultiplayerRoomUser CreateUser(int userId) => new MultiplayerRoomUser(userId);
protected abstract MultiplayerGameplayLeaderboard CreateLeaderboard();
protected abstract MultiplayerLeaderboardProvider CreateLeaderboardProvider();
private readonly BindableList<int> multiplayerUserIds = new BindableList<int>();
private readonly BindableDictionary<int, SpectatorState> watchedUserStates = new BindableDictionary<int, SpectatorState>();
@@ -124,19 +128,38 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("create leaderboard", () =>
{
Leaderboard?.Expire();
Clear(true);
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
LoadComponentAsync(Leaderboard = CreateLeaderboard(), Add);
LoadComponentAsync(LeaderboardProvider = CreateLeaderboardProvider(), Add);
Add(new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = [(typeof(IGameplayLeaderboardProvider), LeaderboardProvider)],
Child = Leaderboard = new DrawableGameplayLeaderboard
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
});
});
AddUntilStep("wait for load", () => Leaderboard!.IsLoaded);
AddStep("check watch requests were sent", () =>
AddUntilStep("check watch requests were sent", () =>
{
foreach (var user in MultiplayerUsers)
spectatorClient.Verify(s => s.WatchUser(user.UserID), Times.Once);
try
{
foreach (var user in MultiplayerUsers)
spectatorClient.Verify(s => s.WatchUser(user.UserID), Times.Once);
return true;
}
catch (MockException)
{
return false;
}
});
}
@@ -159,10 +182,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
return false;
});
AddStep("check stop watching requests were sent", () =>
AddUntilStep("check stop watching requests were sent", () =>
{
foreach (var user in MultiplayerUsers)
spectatorClient.Verify(s => s.StopWatchingUser(user.UserID), Times.Once);
try
{
foreach (var user in MultiplayerUsers)
spectatorClient.Verify(s => s.StopWatchingUser(user.UserID), Times.Once);
return true;
}
catch (MockException)
{
return false;
}
});
}
@@ -204,12 +235,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
header.Combo++;
header.MaxCombo = Math.Max(header.MaxCombo, header.Combo);
header.Statistics[HitResult.Meh]++;
header.TotalScore += 50;
break;
default:
header.Combo++;
header.MaxCombo = Math.Max(header.MaxCombo, header.Combo);
header.Statistics[HitResult.Great]++;
header.TotalScore += 300;
break;
}
@@ -218,3 +251,4 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
}
}
@@ -61,6 +61,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("import beatmap", () =>
{
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
Realm.Write(r =>
{
foreach (var beatmapInfo in r.All<BeatmapInfo>())
beatmapInfo.OnlineMD5Hash = beatmapInfo.MD5Hash;
});
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
InitialBeatmap = importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0);
OtherBeatmap = importedSet.Beatmaps.Last(b => b.Ruleset.OnlineID == 0);
@@ -13,6 +13,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.Lounge;
@@ -30,7 +31,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Cached]
protected readonly OverlayColourProvider ColourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
private DrawableLoungeRoom drawableRoom = null!;
private LoungeRoomPanel panel = null!;
private SearchTextBox searchTextBox = null!;
private readonly ManualResetEventSlim allowResponseCallback = new ManualResetEventSlim();
@@ -40,14 +41,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
var mockLounge = new Mock<IOnlinePlayLounge>();
mockLounge
.Setup(l => l.Join(It.IsAny<Room>(), It.IsAny<string>(), It.IsAny<Action<Room>>(), It.IsAny<Action<string>>()))
.Callback<Room, string, Action<Room>, Action<string>>((_, _, _, d) =>
.Setup(l => l.Join(It.IsAny<Room>(), It.IsAny<string>(), It.IsAny<Action<Room>>(), It.IsAny<Action<string, Exception?>>()))
.Callback<Room, string, Action<Room>, Action<string, Exception?>>((_, _, _, d) =>
{
Task.Run(() =>
{
allowResponseCallback.Wait(10000);
allowResponseCallback.Reset();
Schedule(() => d?.Invoke("Incorrect password"));
Schedule(() => d?.Invoke("Incorrect password", new InvalidPasswordException()));
});
});
@@ -73,7 +74,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
Width = 500,
Depth = float.MaxValue
},
drawableRoom = new DrawableLoungeRoom(room)
panel = new LoungeRoomPanel(room)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -87,16 +88,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestFocusViaKeyboardCommit()
{
DrawableLoungeRoom.PasswordEntryPopover? popover = null;
LoungeRoomPanel.PasswordEntryPopover? popover = null;
AddAssert("search textbox has focus", () => checkFocus(searchTextBox));
AddStep("click room twice", () =>
{
InputManager.MoveMouseTo(drawableRoom);
InputManager.MoveMouseTo(panel);
InputManager.Click(MouseButton.Left);
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for popover", () => (popover = InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().SingleOrDefault()) != null);
AddUntilStep("wait for popover", () => (popover = InputManager.ChildrenOfType<LoungeRoomPanel.PasswordEntryPopover>().SingleOrDefault()) != null);
AddAssert("textbox has focus", () => checkFocus(popover.ChildrenOfType<OsuPasswordTextBox>().Single()));
@@ -122,16 +123,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestFocusViaMouseCommit()
{
DrawableLoungeRoom.PasswordEntryPopover? popover = null;
LoungeRoomPanel.PasswordEntryPopover? popover = null;
AddAssert("search textbox has focus", () => checkFocus(searchTextBox));
AddStep("click room twice", () =>
{
InputManager.MoveMouseTo(drawableRoom);
InputManager.MoveMouseTo(panel);
InputManager.Click(MouseButton.Left);
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for popover", () => (popover = InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().SingleOrDefault()) != null);
AddUntilStep("wait for popover", () => (popover = InputManager.ChildrenOfType<LoungeRoomPanel.PasswordEntryPopover>().SingleOrDefault()) != null);
AddAssert("textbox has focus", () => checkFocus(popover.ChildrenOfType<OsuPasswordTextBox>().Single()));
@@ -105,6 +105,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
}
[Test]
public void TestMarkCompleted()
{
createPlaylist();
AddStep("mark some items as complete", () =>
{
playlist.Items[0].MarkCompleted();
playlist.Items[2].MarkCompleted();
playlist.Items[3].MarkCompleted();
playlist.Items[5].MarkCompleted();
});
}
[Test]
public void TestSelectable()
{
@@ -105,7 +105,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("select other beatmap", () => ((Screens.Select.SongSelect)CurrentSubScreen).FinaliseSelection(otherBeatmap = beatmap()));
AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen);
AddUntilStep("selected item is new beatmap", () => (CurrentSubScreen as MultiplayerMatchSubScreen)?.SelectedItem.Value?.Beatmap.OnlineID == otherBeatmap.OnlineID);
AddUntilStep("selected item is new beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == otherBeatmap.OnlineID);
}
private void addItem(Func<BeatmapInfo> beatmap)
@@ -9,15 +9,16 @@ using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Select.Leaderboards;
namespace osu.Game.Tests.Visual.Multiplayer
{
public partial class TestSceneMultiSpectatorLeaderboard : MultiplayerTestScene
{
private Dictionary<int, ManualClock> clocks = null!;
private MultiSpectatorLeaderboard? leaderboard;
private MultiSpectatorLeaderboardProvider? leaderboardProvider;
private DrawableGameplayLeaderboard leaderboard = null!;
[SetUpSteps]
public override void SetUpSteps()
@@ -29,7 +30,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("reset", () =>
{
leaderboard?.RemoveAndDisposeImmediately();
Clear(true);
clocks = new Dictionary<int, ManualClock>
{
@@ -48,21 +49,27 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(clocks.Keys.Select(id => new MultiplayerRoomUser(id)).ToArray())
LoadComponentAsync(leaderboardProvider = new MultiSpectatorLeaderboardProvider(clocks.Keys.Select(id => new MultiplayerRoomUser(id)).ToArray()), Add);
Add(new DependencyProvidingContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Expanded = { Value = true }
}, Add);
RelativeSizeAxes = Axes.Both,
CachedDependencies = [(typeof(IGameplayLeaderboardProvider), leaderboardProvider)],
Child = leaderboard = new DrawableGameplayLeaderboard
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Expanded = { Value = true }
}
});
});
AddUntilStep("wait for load", () => leaderboard!.IsLoaded);
AddUntilStep("wait for user population", () => leaderboard.ChildrenOfType<GameplayLeaderboardScore>().Count() == 2);
AddUntilStep("wait for load", () => leaderboard.IsLoaded);
AddUntilStep("wait for user population", () => leaderboard.ChildrenOfType<DrawableGameplayLeaderboardScore>().Count() == 2);
AddStep("add clock sources", () =>
{
foreach ((int userId, var clock) in clocks)
leaderboard!.AddClock(userId, clock);
leaderboardProvider!.AddClock(userId, clock);
});
}
@@ -123,6 +130,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
=> AddStep($"set user {userId} time {time}", () => clocks[userId].CurrentTime = time);
private void assertCombo(int userId, int expectedCombo)
=> AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType<GameplayLeaderboardScore>().Single(s => s.User?.OnlineID == userId).Combo.Value == expectedCombo);
=> AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType<DrawableGameplayLeaderboardScore>().Single(s => s.User?.OnlineID == userId).Combo.Value == expectedCombo);
}
}
@@ -560,7 +560,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private PlayerArea getInstance(int userId) => spectatorScreen.ChildrenOfType<PlayerArea>().Single(p => p.UserId == userId);
private GameplayLeaderboardScore getLeaderboardScore(int userId) => spectatorScreen.ChildrenOfType<GameplayLeaderboardScore>().Single(s => s.User?.OnlineID == userId);
private DrawableGameplayLeaderboardScore getLeaderboardScore(int userId) => spectatorScreen.ChildrenOfType<DrawableGameplayLeaderboardScore>().Single(s => s.User?.OnlineID == userId);
private int[] getPlayerIds(int count) => Enumerable.Range(PLAYER_1_ID, count).ToArray();
}
@@ -81,6 +81,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("import beatmap", () =>
{
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
Realm.Write(r =>
{
foreach (var beatmapInfo in r.All<BeatmapInfo>())
beatmapInfo.OnlineMD5Hash = beatmapInfo.MD5Hash;
});
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
});
@@ -269,7 +274,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
AddStep("refresh rooms", () => this.ChildrenOfType<LoungeSubScreen>().Single().UpdateFilter());
AddUntilStep("wait for room", () => this.ChildrenOfType<DrawableRoom>().Any());
AddUntilStep("wait for room", () => this.ChildrenOfType<RoomPanel>().Any());
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("join room and immediately exit select", () =>
@@ -298,7 +303,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
AddStep("refresh rooms", () => this.ChildrenOfType<LoungeSubScreen>().Single().UpdateFilter());
AddUntilStep("wait for room", () => this.ChildrenOfType<DrawableRoom>().Any());
AddUntilStep("wait for room", () => this.ChildrenOfType<RoomPanel>().Any());
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("join room", () => InputManager.Key(Key.Enter));
@@ -349,13 +354,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
AddStep("refresh rooms", () => this.ChildrenOfType<LoungeSubScreen>().Single().UpdateFilter());
AddUntilStep("wait for room", () => this.ChildrenOfType<DrawableRoom>().Any());
AddUntilStep("wait for room", () => this.ChildrenOfType<RoomPanel>().Any());
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("join room", () => InputManager.Key(Key.Enter));
DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null;
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().FirstOrDefault()) != null);
LoungeRoomPanel.PasswordEntryPopover? passwordEntryPopover = null;
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<LoungeRoomPanel.PasswordEntryPopover>().FirstOrDefault()) != null);
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType<TextBox>().First().Text = "password");
AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType<OsuButton>().First().TriggerClick());
@@ -438,7 +443,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("Enter song select", () =>
{
var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerComponents.CurrentScreen).CurrentSubScreen;
((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(item);
((MultiplayerMatchSubScreen)currentSubScreen).ShowSongSelect(item);
});
AddUntilStep("wait for song select", () => this.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true);
@@ -479,7 +484,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("Enter song select", () =>
{
var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerComponents.CurrentScreen).CurrentSubScreen;
((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(item);
((MultiplayerMatchSubScreen)currentSubScreen).ShowSongSelect(item);
});
AddUntilStep("wait for song select", () => this.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true);
@@ -520,7 +525,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("Enter song select", () =>
{
var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerComponents.CurrentScreen).CurrentSubScreen;
((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(item);
((MultiplayerMatchSubScreen)currentSubScreen).ShowSongSelect(item);
});
AddUntilStep("wait for song select", () => this.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true);
@@ -652,7 +657,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("invoke on back button", () => multiplayerComponents.OnBackButton());
AddAssert("mod overlay is hidden", () => this.ChildrenOfType<RoomSubScreen>().Single().UserModsSelectOverlay.State.Value == Visibility.Hidden);
AddAssert("mod overlay is hidden", () => this.ChildrenOfType<MultiplayerUserModSelectOverlay>().Single().State.Value == Visibility.Hidden);
AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden);
@@ -802,7 +807,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
AddStep("refresh rooms", () => this.ChildrenOfType<LoungeSubScreen>().Single().UpdateFilter());
AddUntilStep("wait for room", () => this.ChildrenOfType<DrawableRoom>().Any());
AddUntilStep("wait for room", () => this.ChildrenOfType<RoomPanel>().Any());
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("disable polling", () => this.ChildrenOfType<LoungeListingPoller>().Single().TimeBetweenPolls.Value = 0);
@@ -823,11 +828,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddUntilStep("wait for join", () => multiplayerClient.RoomJoined);
AddAssert("local room has correct settings", () =>
{
var localRoom = this.ChildrenOfType<MultiplayerMatchSubScreen>().Single().Room;
return localRoom.Name == multiplayerClient.ServerSideRooms[0].Name && localRoom.Playlist.Single().ID == 2;
});
AddAssert("local room has correct name", () => this.ChildrenOfType<MultiplayerRoomPanel>().Single().Room.Name, () => Is.EqualTo(multiplayerClient.ServerSideRooms[0].Name));
AddAssert("local room has correct playlist", () => this.ChildrenOfType<MultiplayerQueueList>().Single().Items.Single().ID, () => Is.EqualTo(2));
}
[Test]
@@ -1054,6 +1056,45 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("hidden is selected", () => SelectedMods.Value, () => Has.One.TypeOf(typeof(OsuModHidden)));
}
[FlakyTest]
[Test]
public void TestGlobalBeatmapDoesNotChangeAtResults()
{
createRoom(() => new Room
{
Name = "Test Room",
QueueMode = QueueMode.AllPlayers,
Playlist =
[
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
AllowedMods = new[] { new APIMod { Acronym = "HD" } },
},
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 1)).BeatmapInfo)
{
RulesetID = new TaikoRuleset().RulesetInfo.OnlineID,
AllowedMods = new[] { new APIMod { Acronym = "HD" } },
},
]
});
enterGameplay();
// Gameplay runs in real-time, so we need to incrementally check if gameplay has finished in order to not time out.
for (double i = 1000; i < TestResources.QUICK_BEATMAP_LENGTH; i += 1000)
{
double time = i;
AddUntilStep($"wait for time > {i}", () => this.ChildrenOfType<GameplayClockContainer>().SingleOrDefault()?.CurrentTime > time);
}
AddUntilStep("wait for results", () => multiplayerComponents.CurrentScreen is ResultsScreen);
AddAssert("global beatmap still matches first playlist item", () => Beatmap.Value.BeatmapInfo.OnlineID, () => Is.EqualTo(multiplayerClient.ClientRoom!.Playlist[0].BeatmapID));
AddStep("return to match", () => multiplayerComponents.Exit());
AddAssert("global beatmap matches second playlist item", () => Beatmap.Value.BeatmapInfo.OnlineID, () => Is.EqualTo(multiplayerClient.ClientRoom!.Playlist[1].BeatmapID));
}
private void enterGameplay()
{
pressReadyButton();
@@ -9,7 +9,7 @@ using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Select.Leaderboards;
namespace osu.Game.Tests.Visual.Multiplayer
{
@@ -25,27 +25,25 @@ namespace osu.Game.Tests.Visual.Multiplayer
return user;
}
protected override MultiplayerGameplayLeaderboard CreateLeaderboard()
{
return new TestLeaderboard(MultiplayerUsers.ToArray())
protected override MultiplayerLeaderboardProvider CreateLeaderboardProvider() =>
new TestLeaderboard(MultiplayerUsers.ToArray())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
}
[Test]
public void TestPerUserMods()
{
AddStep("first user has no mods", () => Assert.That(((TestLeaderboard)Leaderboard!).UserMods[0], Is.Empty));
AddStep("first user has no mods", () => Assert.That(((TestLeaderboard)LeaderboardProvider!).UserMods[0], Is.Empty));
AddStep("last user has NF mod", () =>
{
Assert.That(((TestLeaderboard)Leaderboard!).UserMods[TOTAL_USERS - 1], Has.One.Items);
Assert.That(((TestLeaderboard)Leaderboard).UserMods[TOTAL_USERS - 1].Single(), Is.TypeOf<OsuModNoFail>());
Assert.That(((TestLeaderboard)LeaderboardProvider!).UserMods[TOTAL_USERS - 1], Has.One.Items);
Assert.That(((TestLeaderboard)LeaderboardProvider).UserMods[TOTAL_USERS - 1].Single(), Is.TypeOf<OsuModNoFail>());
});
}
private partial class TestLeaderboard : MultiplayerGameplayLeaderboard
private partial class TestLeaderboard : MultiplayerLeaderboardProvider
{
public Dictionary<int, IReadOnlyList<Mod>> UserMods => UserScores.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ScoreProcessor.Mods);
@@ -7,6 +7,7 @@ using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Select.Leaderboards;
namespace osu.Game.Tests.Visual.Multiplayer
{
@@ -24,8 +25,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
return user;
}
protected override MultiplayerGameplayLeaderboard CreateLeaderboard() =>
new MultiplayerGameplayLeaderboard(MultiplayerUsers.ToArray())
protected override MultiplayerLeaderboardProvider CreateLeaderboardProvider() =>
new MultiplayerLeaderboardProvider(MultiplayerUsers.ToArray())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -39,17 +40,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
LoadComponentAsync(new MatchScoreDisplay
{
Team1Score = { BindTarget = Leaderboard!.TeamScores[0] },
Team2Score = { BindTarget = Leaderboard.TeamScores[1] }
Team1Score = { BindTarget = LeaderboardProvider!.TeamScores[0] },
Team2Score = { BindTarget = LeaderboardProvider.TeamScores[1] }
}, Add);
LoadComponentAsync(new GameplayMatchScoreDisplay
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Team1Score = { BindTarget = Leaderboard.TeamScores[0] },
Team2Score = { BindTarget = Leaderboard.TeamScores[1] },
Expanded = { BindTarget = Leaderboard.Expanded },
Team1Score = { BindTarget = LeaderboardProvider.TeamScores[0] },
Team2Score = { BindTarget = LeaderboardProvider.TeamScores[1] },
Expanded = { BindTarget = Leaderboard!.Expanded },
}, Add);
});
}
@@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().Any());
AddUntilStep("password prompt appeared", () => InputManager.ChildrenOfType<LoungeRoomPanel.PasswordEntryPopover>().Any());
AddAssert("textbox has focus", () => InputManager.FocusedDrawable is OsuPasswordTextBox);
@@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("textbox lost focus", () => InputManager.FocusedDrawable is SearchTextBox);
AddStep("hit escape", () => InputManager.Key(Key.Escape));
AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().Any());
AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType<LoungeRoomPanel.PasswordEntryPopover>().Any());
AddAssert("room not joined", () => !MultiplayerClient.RoomJoined);
}
@@ -65,9 +65,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().Any());
AddUntilStep("password prompt appeared", () => InputManager.ChildrenOfType<LoungeRoomPanel.PasswordEntryPopover>().Any());
AddStep("exit screen", () => Stack.Exit());
AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().Any());
AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType<LoungeRoomPanel.PasswordEntryPopover>().Any());
AddAssert("room not joined", () => !MultiplayerClient.RoomJoined);
}
@@ -75,12 +75,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestJoinRoomWithIncorrectPasswordViaButton()
{
DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null;
LoungeRoomPanel.PasswordEntryPopover? passwordEntryPopover = null;
createRooms(GenerateRooms(1, withPassword: true));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().FirstOrDefault()) != null);
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<LoungeRoomPanel.PasswordEntryPopover>().FirstOrDefault()) != null);
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType<TextBox>().First().Text = "wrong");
AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType<OsuButton>().First().TriggerClick());
@@ -94,12 +94,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestJoinRoomWithIncorrectPasswordViaEnter()
{
DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null;
LoungeRoomPanel.PasswordEntryPopover? passwordEntryPopover = null;
createRooms(GenerateRooms(1, withPassword: true));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().FirstOrDefault()) != null);
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<LoungeRoomPanel.PasswordEntryPopover>().FirstOrDefault()) != null);
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType<TextBox>().First().Text = "wrong");
AddStep("press enter", () => InputManager.Key(Key.Enter));
@@ -113,12 +113,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestJoinRoomWithCorrectPassword()
{
DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null;
LoungeRoomPanel.PasswordEntryPopover? passwordEntryPopover = null;
createRooms(GenerateRooms(1, withPassword: true));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().FirstOrDefault()) != null);
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<LoungeRoomPanel.PasswordEntryPopover>().FirstOrDefault()) != null);
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType<TextBox>().First().Text = "password");
AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType<OsuButton>().First().TriggerClick());
@@ -128,12 +128,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestJoinRoomWithPasswordViaKeyboardOnly()
{
DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null;
LoungeRoomPanel.PasswordEntryPopover? passwordEntryPopover = null;
createRooms(GenerateRooms(1, withPassword: true));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().FirstOrDefault()) != null);
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<LoungeRoomPanel.PasswordEntryPopover>().FirstOrDefault()) != null);
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType<TextBox>().First().Text = "password");
AddStep("press enter", () => InputManager.Key(Key.Enter));
@@ -1,12 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Extensions;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
@@ -185,7 +187,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("mod select contents loaded",
() => this.ChildrenOfType<ModColumn>().Any() && this.ChildrenOfType<ModColumn>().All(col => col.IsLoaded && col.ItemsLoaded));
AddUntilStep("mod select contains only double time mod",
() => this.ChildrenOfType<RoomSubScreen>().Single().UserModsSelectOverlay
() => this.ChildrenOfType<MultiplayerUserModSelectOverlay>().Single()
.ChildrenOfType<ModPanel>()
.SingleOrDefault(panel => panel.Visible)?.Mod is OsuModDoubleTime);
}
@@ -211,7 +213,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("press toggle mod select key", () => InputManager.Key(Key.F1));
AddUntilStep("mod select shown", () => this.ChildrenOfType<RoomSubScreen>().Single().UserModsSelectOverlay.State.Value == Visibility.Visible);
AddUntilStep("mod select shown", () => this.ChildrenOfType<MultiplayerUserModSelectOverlay>().Single().State.Value == Visibility.Visible);
}
[Test]
@@ -234,7 +236,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("press toggle mod select key", () => InputManager.Key(Key.F1));
AddWaitStep("wait some", 3);
AddAssert("mod select not shown", () => this.ChildrenOfType<RoomSubScreen>().Single().UserModsSelectOverlay.State.Value == Visibility.Hidden);
AddAssert("mod select not shown", () => this.ChildrenOfType<MultiplayerUserModSelectOverlay>().Single().State.Value == Visibility.Hidden);
}
[Test]
@@ -306,10 +308,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("mod select shows unranked", () => this.ChildrenOfType<RankingInformationDisplay>().Single().Ranked.Value == false);
AddAssert("score multiplier = 1.20", () => this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01));
AddStep("select flashlight", () => screen.UserModsSelectOverlay.ChildrenOfType<ModPanel>().Single(m => m.Mod is ModFlashlight).TriggerClick());
AddStep("select flashlight", () => this.ChildrenOfType<MultiplayerUserModSelectOverlay>().Single().ChildrenOfType<ModPanel>().Single(m => m.Mod is ModFlashlight).TriggerClick());
AddAssert("score multiplier = 1.35", () => this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(1.35).Within(0.01));
AddStep("change flashlight setting", () => ((OsuModFlashlight)screen.UserModsSelectOverlay.SelectedMods.Value.Single()).FollowDelay.Value = 1200);
AddStep("change flashlight setting", () => ((OsuModFlashlight)this.ChildrenOfType<MultiplayerUserModSelectOverlay>().Single().SelectedMods.Value.Single()).FollowDelay.Value = 1200);
AddAssert("score multiplier = 1.20", () => this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01));
}
@@ -330,10 +332,98 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for join", () => RoomJoined);
AddUntilStep("button visible", () => this.ChildrenOfType<DrawableMatchRoom>().Single().ChangeSettingsButton?.Alpha, () => Is.GreaterThan(0));
AddUntilStep("button visible", () => this.ChildrenOfType<MultiplayerRoomPanel>().Single().ChangeSettingsButton.Alpha, () => Is.GreaterThan(0));
AddStep("join other user", void () => MultiplayerClient.AddUser(new APIUser { Id = PLAYER_1_ID }));
AddStep("make other user host", () => MultiplayerClient.TransferHost(PLAYER_1_ID));
AddAssert("button hidden", () => this.ChildrenOfType<DrawableMatchRoom>().Single().ChangeSettingsButton?.Alpha, () => Is.EqualTo(0));
AddAssert("button hidden", () => this.ChildrenOfType<MultiplayerRoomPanel>().Single().ChangeSettingsButton.Alpha, () => Is.EqualTo(0));
}
[Test]
public void TestUserModSelectUpdatesWhenNotVisible()
{
AddStep("add playlist item", () =>
{
room.Playlist =
[
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
AllowedMods = [new APIMod(new OsuModFlashlight())]
}
];
});
ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
AddUntilStep("wait for join", () => RoomJoined);
// 1. Open the mod select overlay and enable flashlight
ClickButtonWhenEnabled<UserModSelectButton>();
AddUntilStep("mod select contents loaded", () => this.ChildrenOfType<ModColumn>().Any() && this.ChildrenOfType<ModColumn>().All(col => col.IsLoaded && col.ItemsLoaded));
AddStep("click flashlight panel", () =>
{
ModPanel panel = this.ChildrenOfType<ModPanel>().Single(p => p.Mod is OsuModFlashlight);
InputManager.MoveMouseTo(panel);
InputManager.Click(MouseButton.Left);
});
AddUntilStep("flashlight mod enabled", () => MultiplayerClient.ClientRoom!.Users[0].Mods.Any());
// 2. Close the mod select overlay, edit the playlist to disable allowed mods, and then edit it again to re-enable allowed mods.
AddStep("close mod select overlay", () => this.ChildrenOfType<MultiplayerUserModSelectOverlay>().Single().Hide());
AddUntilStep("mod select overlay not present", () => !this.ChildrenOfType<MultiplayerUserModSelectOverlay>().Single().IsPresent);
AddStep("disable allowed mods", () => MultiplayerClient.EditPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem(MultiplayerClient.ServerRoom!.Playlist[0])
{
AllowedMods = []
})));
// This would normally be done as part of the above operation with an actual server.
AddStep("disable user mods", () => MultiplayerClient.ChangeUserMods(API.LocalUser.Value.OnlineID, Array.Empty<APIMod>()));
AddUntilStep("flashlight mod disabled", () => !MultiplayerClient.ClientRoom!.Users[0].Mods.Any());
AddStep("re-enable allowed mods", () => MultiplayerClient.EditPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem(MultiplayerClient.ServerRoom!.Playlist[0])
{
AllowedMods = [new APIMod(new OsuModFlashlight())]
})));
AddAssert("flashlight mod still disabled", () => !MultiplayerClient.ClientRoom!.Users[0].Mods.Any());
// 3. Open the mod select overlay, check that the flashlight mod panel is deactivated.
ClickButtonWhenEnabled<UserModSelectButton>();
AddUntilStep("mod select contents loaded", () => this.ChildrenOfType<ModColumn>().Any() && this.ChildrenOfType<ModColumn>().All(col => col.IsLoaded && col.ItemsLoaded));
AddAssert("flashlight mod still disabled", () => !MultiplayerClient.ClientRoom!.Users[0].Mods.Any());
AddAssert("flashlight mod panel not activated", () => !this.ChildrenOfType<ModPanel>().Single(p => p.Mod is OsuModFlashlight).Active.Value);
}
[Test]
public void TestStartCountdown()
{
AddStep("set playlist", () =>
{
room.Playlist =
[
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID
}
];
});
ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
AddUntilStep("wait for room join", () => RoomJoined);
AddStep("click countdown button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerCountdownButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddStep("start a countdown", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<Popover>().Single().ChildrenOfType<Button>().First());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("countdown started", () => MultiplayerClient.ServerRoom!.ActiveCountdowns.Any());
}
private partial class TestMultiplayerMatchSubScreen : MultiplayerMatchSubScreen
@@ -6,7 +6,6 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Platform;
@@ -32,7 +31,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
private BeatmapManager beatmaps = null!;
private BeatmapSetInfo importedSet = null!;
private BeatmapInfo importedBeatmap = null!;
private Room room = null!;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
@@ -47,19 +45,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
base.SetUpSteps();
AddStep("create room", () => room = CreateDefaultRoom());
AddStep("join room", () => JoinRoom(room));
AddStep("join room", () => JoinRoom(CreateDefaultRoom()));
WaitForJoined();
AddStep("create list", () =>
{
Child = list = new MultiplayerPlaylist(room)
Child = list = new MultiplayerPlaylist
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.4f, 0.8f),
SelectedItem = new Bindable<PlaylistItem?>()
Size = new Vector2(0.4f, 0.8f)
};
});
@@ -158,37 +154,36 @@ namespace osu.Game.Tests.Visual.Multiplayer
assertQueueTabCount(0);
}
[Ignore("Expired items are initially removed from the room.")]
[Test]
public void TestJoinRoomWithMixedItemsAddedInCorrectLists()
{
AddStep("leave room", () => MultiplayerClient.LeaveRoom());
AddUntilStep("wait for room part", () => !RoomJoined);
AddStep("join room with items", () =>
AddStep("join room with expired items", () =>
{
API.Queue(new CreateRoomRequest(new Room
{
Name = "test name",
Playlist =
[
new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo)
{
RulesetID = Ruleset.Value.OnlineID
},
new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo)
{
RulesetID = Ruleset.Value.OnlineID,
Expired = true
}
]
}));
Room room = CreateDefaultRoom();
room.Playlist =
[
new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo)
{
RulesetID = Ruleset.Value.OnlineID
},
new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo)
{
RulesetID = Ruleset.Value.OnlineID,
Expired = true
}
];
JoinRoom(room);
});
AddUntilStep("wait for room join", () => RoomJoined);
WaitForJoined();
assertItemInQueueListStep(1, 0);
assertItemInHistoryListStep(2, 0);
// IDs are offset by 1 because we've joined two rooms in this test.
assertItemInQueueListStep(2, 0);
assertItemInHistoryListStep(3, 0);
}
[Test]
@@ -49,13 +49,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("create playlist", () =>
{
Child = playlist = new MultiplayerQueueList(room)
Child = playlist = new MultiplayerQueueList
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(500, 300),
Size = new Vector2(500, 300)
};
playlist.Items.ReplaceRange(0, playlist.Items.Count, MultiplayerClient.ClientAPIRoom!.Playlist);
MultiplayerClient.ClientAPIRoom!.PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(Room.Playlist))
@@ -132,6 +134,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
assertDeleteButtonVisibility(1, false);
}
[Test]
public void TestChangeExistingItem()
{
AddStep("change beatmap", () => MultiplayerClient.EditPlaylistItem(new MultiplayerPlaylistItem
{
ID = playlist.Items[0].ID,
BeatmapID = 1337
}).WaitSafely());
AddUntilStep("first playlist item has new beatmap", () => playlist.Items[0].Beatmap.OnlineID, () => Is.EqualTo(1337));
}
private void addPlaylistItem(Func<int> userId)
{
long itemId = -1;
@@ -0,0 +1,47 @@
// 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.Graphics;
using osu.Framework.Testing;
using osu.Game.Online.API;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.OnlinePlay.Multiplayer;
namespace osu.Game.Tests.Visual.Multiplayer
{
public partial class TestSceneMultiplayerUserModDisplay : MultiplayerTestScene
{
private MultiplayerUserModDisplay modDisplay = null!;
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("join room", () => JoinRoom(CreateDefaultRoom()));
WaitForJoined();
AddStep("add display", () => Child = modDisplay = new MultiplayerUserModDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
});
}
[Test]
public void TestChangeMods()
{
AddStep("set DT", () => MultiplayerClient.ChangeUserMods([new OsuModDoubleTime()]).WaitSafely());
AddUntilStep("mod displayed", () => modDisplay.ChildrenOfType<ModIcon>().Count() == 1);
AddStep("set DT, HR", () => MultiplayerClient.ChangeUserMods([new OsuModDoubleTime(), new OsuModHardRock()]).WaitSafely());
AddUntilStep("mods displayed", () => modDisplay.ChildrenOfType<ModIcon>().Count() == 2);
AddStep("set no mods", () => MultiplayerClient.ChangeUserMods(Enumerable.Empty<APIMod>()).WaitSafely());
AddUntilStep("no mods displayed", () => !modDisplay.ChildrenOfType<ModIcon>().Any());
}
}
}
@@ -201,6 +201,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
private bool checkRoomSelected(Room? room) => selectedRoom.Value == room;
private Room? getRoomInFlow(int index) =>
(container.ChildrenOfType<FillFlowContainer<DrawableLoungeRoom>>().First().FlowingChildren.ElementAt(index) as DrawableRoom)?.Room;
(container.ChildrenOfType<FillFlowContainer<LoungeRoomPanel>>().First().FlowingChildren.ElementAt(index) as RoomPanel)?.Room;
}
}
@@ -18,13 +18,13 @@ using osu.Game.Overlays;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Tests.Beatmaps;
using osuTK;
namespace osu.Game.Tests.Visual.Multiplayer
{
public partial class TestSceneDrawableRoom : OsuTestScene
public partial class TestSceneRoomPanel : OsuTestScene
{
[Cached]
protected readonly OverlayColourProvider ColourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
@@ -129,24 +129,24 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestEnableAndDisablePassword()
{
DrawableRoom drawableRoom = null!;
RoomPanel panel = null!;
Room room = null!;
AddStep("create room", () => Child = drawableRoom = createLoungeRoom(room = new Room
AddStep("create room", () => Child = panel = createLoungeRoom(room = new Room
{
Name = "Room with password",
Type = MatchType.HeadToHead,
}));
AddUntilStep("wait for panel load", () => drawableRoom.ChildrenOfType<DrawableRoomParticipantsList>().Any());
AddUntilStep("wait for panel load", () => panel.ChildrenOfType<DrawableRoomParticipantsList>().Any());
AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType<DrawableRoom.PasswordProtectedIcon>().Single().Alpha));
AddAssert("password icon hidden", () => Precision.AlmostEquals(0, panel.ChildrenOfType<RoomPanel.PasswordProtectedIcon>().Single().Alpha));
AddStep("set password", () => room.Password = "password");
AddAssert("password icon visible", () => Precision.AlmostEquals(1, drawableRoom.ChildrenOfType<DrawableRoom.PasswordProtectedIcon>().Single().Alpha));
AddAssert("password icon visible", () => Precision.AlmostEquals(1, panel.ChildrenOfType<RoomPanel.PasswordProtectedIcon>().Single().Alpha));
AddStep("unset password", () => room.Password = string.Empty);
AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType<DrawableRoom.PasswordProtectedIcon>().Single().Alpha));
AddAssert("password icon hidden", () => Precision.AlmostEquals(0, panel.ChildrenOfType<RoomPanel.PasswordProtectedIcon>().Single().Alpha));
}
[Test]
@@ -160,38 +160,29 @@ namespace osu.Game.Tests.Visual.Multiplayer
Spacing = new Vector2(5),
Children = new[]
{
new DrawableMatchRoom(new Room
new MultiplayerRoomPanel(new Room
{
Name = "A host-only room",
QueueMode = QueueMode.HostOnly,
Type = MatchType.HeadToHead,
})
{
SelectedItem = new Bindable<PlaylistItem?>()
},
new DrawableMatchRoom(new Room
}),
new MultiplayerRoomPanel(new Room
{
Name = "An all-players, team-versus room",
QueueMode = QueueMode.AllPlayers,
Type = MatchType.TeamVersus
})
{
SelectedItem = new Bindable<PlaylistItem?>()
},
new DrawableMatchRoom(new Room
}),
new MultiplayerRoomPanel(new Room
{
Name = "A round-robin room",
QueueMode = QueueMode.AllPlayersRoundRobin,
Type = MatchType.HeadToHead
})
{
SelectedItem = new Bindable<PlaylistItem?>()
},
}),
}
});
}
private DrawableRoom createLoungeRoom(Room room)
private RoomPanel createLoungeRoom(Room room)
{
room.Host ??= new APIUser { Username = "peppy", Id = 2 };
@@ -204,7 +195,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
}).ToArray();
}
return new DrawableLoungeRoom(room)
return new LoungeRoomPanel(room)
{
MatchingFilter = true,
SelectedRoom = selectedRoom
@@ -33,6 +33,10 @@ using osu.Game.Overlays.BeatmapListing;
using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Notifications;
using osu.Game.Overlays.Toolbar;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Scoring;
@@ -394,6 +398,60 @@ namespace osu.Game.Tests.Visual.Navigation
}
}
[Test]
public void TestScrollSpeedAdjustDuringGameplay()
{
Player player = null;
Screens.Select.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game).WaitSafely());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
AddStep("switch to mania ruleset", () =>
{
InputManager.PressKey(Key.LControl);
InputManager.Key(Key.Number4);
InputManager.ReleaseKey(Key.LControl);
});
AddStep("set mods", () => Game.SelectedMods.Value = new Mod[] { new OsuModNoFail() });
AddStep("press enter", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for player", () =>
{
DismissAnyNotifications();
player = Game.ScreenStack.CurrentScreen as Player;
return player?.IsLoaded == true;
});
AddUntilStep("wait for track playing", () => Game.Beatmap.Value.Track.IsRunning);
checkScrollSpeed(8, 8);
AddStep("adjust scroll speed via keyboard", () => InputManager.Key(Key.F4));
checkScrollSpeed(9, 9);
AddStep("seek beyond 10 seconds", () => player.ChildrenOfType<GameplayClockContainer>().First().Seek(10500));
AddUntilStep("wait for seek", () => player.ChildrenOfType<GameplayClockContainer>().First().CurrentTime, () => Is.GreaterThan(10600));
AddStep("attempt adjust offset via keyboard", () => InputManager.Key(Key.F4));
checkScrollSpeed(9, 9);
AddStep("attempt adjust offset via config change", () => getConfigManager().SetValue(ManiaRulesetSetting.ScrollSpeed, 10.0));
checkScrollSpeed(10, 9);
void checkScrollSpeed(double configValue, double gameplayValue)
{
AddUntilStep($"config value is {configValue}", () => getConfigManager().Get<double>(ManiaRulesetSetting.ScrollSpeed), () => Is.EqualTo(configValue));
AddUntilStep($"gameplay value is {gameplayValue}", () => this.ChildrenOfType<DrawableManiaRuleset>().Single().TargetTimeRange,
() => Is.EqualTo(DrawableManiaRuleset.ComputeScrollTime(gameplayValue)));
}
ManiaRulesetConfigManager getConfigManager() => ((ManiaRulesetConfigManager)Game.Dependencies.Get<IRulesetConfigCache>().GetConfigFor(new ManiaRuleset())!);
}
[Test]
public void TestOffsetAdjustDuringGameplay()
{
@@ -99,8 +99,35 @@ namespace osu.Game.Tests.Visual.Online
Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
},
TopTags =
[
new APIBeatmapTag { TagId = 4, VoteCount = 1 },
new APIBeatmapTag { TagId = 2, VoteCount = 1 },
new APIBeatmapTag { TagId = 23, VoteCount = 5 },
],
},
},
RelatedTags =
[
new APITag
{
Id = 2,
Name = "song representation/simple",
Description = "Accessible and straightforward map design."
},
new APITag
{
Id = 4,
Name = "style/clean",
Description = "Visually uncluttered and organised patterns, often involving few overlaps and equal visual spacing between objects."
},
new APITag
{
Id = 23,
Name = "aim/aim control",
Description = "Patterns with velocity or direction changes which strongly go against a player's natural movement pattern."
}
]
});
});
@@ -215,6 +215,32 @@ namespace osu.Game.Tests.Visual.Online
});
}
[Test]
public void TestChannelCloseViaMiddleClick()
{
var testPMChannel = new Channel(testUser);
AddStep("Show overlay", () => chatOverlay.Show());
joinTestChannel(0);
joinChannel(testPMChannel);
AddStep("Select PM channel", () => clickDrawable(getChannelListItem(testPMChannel)));
AddStep("Middle click", () =>
{
var item = getChannelListItem(testPMChannel);
InputManager.MoveMouseTo(item);
InputManager.Click(MouseButton.Middle);
});
AddAssert("PM channel closed", () => !channelManager.JoinedChannels.Contains(testPMChannel));
AddStep("Select normal channel", () => clickDrawable(getChannelListItem(testChannel1)));
AddStep("Click close button", () =>
{
var item = getChannelListItem(testChannel1);
InputManager.MoveMouseTo(item);
InputManager.Click(MouseButton.Middle);
});
AddAssert("Normal channel closed", () => !channelManager.JoinedChannels.Contains(testChannel1));
}
[Test]
public void TestChannelCloseButton()
{
@@ -61,9 +61,9 @@ namespace osu.Game.Tests.Visual.Playlists
AddUntilStep("last room is not masked", () => checkRoomVisible(roomListing.DrawableRooms[^1]));
}
private bool checkRoomVisible(DrawableRoom room) =>
private bool checkRoomVisible(RoomPanel panel) =>
loungeScreen.ChildrenOfType<OsuScrollContainer>().First().ScreenSpaceDrawQuad
.Contains(room.ScreenSpaceDrawQuad.Centre);
.Contains(panel.ScreenSpaceDrawQuad.Centre);
private void createRooms(params Room[] rooms)
{
@@ -176,6 +176,7 @@ namespace osu.Game.Tests.Visual.Playlists
RulesetID = new OsuRuleset().RulesetInfo.OnlineID
}
];
room.EndDate = DateTimeOffset.Now.AddHours(1);
});
AddAssert("match has default beatmap", () => match.Beatmap.IsDefault);
@@ -212,6 +213,11 @@ namespace osu.Game.Tests.Visual.Playlists
Debug.Assert(beatmap.BeatmapInfo.BeatmapSet != null);
importedBeatmap = manager.Import(beatmap.BeatmapInfo.BeatmapSet)!.Value.Detach();
Realm.Write(r =>
{
foreach (var beatmapInfo in r.All<BeatmapInfo>())
beatmapInfo.OnlineMD5Hash = beatmapInfo.MD5Hash;
});
});
private partial class TestPlaylistsRoomSubScreen : PlaylistsRoomSubScreen
@@ -65,7 +65,8 @@ namespace osu.Game.Tests.Visual.Playlists
OnlineID = 1,
DifficultyName = "Osu 1",
Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
MD5Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
MD5Hash = "11111111",
OnlineMD5Hash = "11111111",
Ruleset = new OsuRuleset().RulesetInfo,
Metadata =
{
@@ -79,7 +80,8 @@ namespace osu.Game.Tests.Visual.Playlists
OnlineID = 2,
DifficultyName = "Osu 2",
Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
MD5Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
MD5Hash = "22222222",
OnlineMD5Hash = "22222222",
Ruleset = new OsuRuleset().RulesetInfo,
Metadata =
{
@@ -93,7 +95,8 @@ namespace osu.Game.Tests.Visual.Playlists
OnlineID = 3,
DifficultyName = "Taiko 1",
Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
MD5Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
MD5Hash = "33333333",
OnlineMD5Hash = "33333333",
Ruleset = new TaikoRuleset().RulesetInfo,
Metadata =
{
@@ -107,7 +110,8 @@ namespace osu.Game.Tests.Visual.Playlists
OnlineID = 4,
DifficultyName = "Taiko 2",
Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
MD5Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
MD5Hash = "44444444",
OnlineMD5Hash = "44444444",
Ruleset = new TaikoRuleset().RulesetInfo,
Metadata =
{
@@ -168,6 +168,19 @@ namespace osu.Game.Tests.Visual.Ranking
};
});
public static List<HitEvent> CreateHitEvents(double offset = 0, int count = 50)
{
var hitEvents = new List<HitEvent>();
for (int i = 0; i < count; i++)
{
for (int j = 0; j < count; j++)
hitEvents.Add(new HitEvent(offset, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null));
}
return hitEvents;
}
public static List<HitEvent> CreateDistributedHitEvents(double centre = 0, double range = 25)
{
var hitEvents = new List<HitEvent>();
@@ -26,6 +26,7 @@ using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mania;
@@ -52,6 +53,9 @@ namespace osu.Game.Tests.Visual.Ranking
private RulesetStore rulesetStore = null!;
private BeatmapManager beatmapManager = null!;
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
@@ -214,14 +218,10 @@ namespace osu.Game.Tests.Visual.Ranking
{
Tags =
[
new APITag { Id = 1, Name = "tech", Description = "Tests uncommon skills.", },
new APITag
{
Id = 2, Name = "alt",
Description = "Colloquial term for maps which use rhythms that encourage the player to alternate notes. Typically distinct from burst or stream maps.",
},
new APITag { Id = 3, Name = "aim", Description = "Category for difficulty relating to cursor movement.", },
new APITag { Id = 4, Name = "tap", Description = "Category for difficulty relating to tapping input.", },
new APITag { Id = 1, Name = "song representation/simple", Description = "Accessible and straightforward map design.", },
new APITag { Id = 2, Name = "style/clean", Description = "Visually uncluttered and organised patterns, often involving few overlaps and equal visual spacing between objects.", },
new APITag { Id = 3, Name = "aim/aim control", Description = "Patterns with velocity or direction changes which strongly go against a player's natural movement pattern.", },
new APITag { Id = 4, Name = "tap/bursts", Description = "Patterns requiring continuous movement and alternating, typically 9 notes or less.", },
]
}), 500);
return true;
@@ -368,12 +368,16 @@ namespace osu.Game.Tests.Visual.Ranking
private void loadPanel(ScoreInfo score) => AddStep("load panel", () =>
{
Child = new StatisticsPanel
Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
Score = { Value = score },
AchievedScore = score,
Child = new StatisticsPanel
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
Score = { Value = score },
AchievedScore = score,
},
};
});
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Testing;
@@ -9,21 +10,23 @@ using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko;
using osu.Game.Screens.Ranking;
namespace osu.Game.Tests.Visual.Ranking
{
public partial class TestSceneUserTagControl : OsuTestScene
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
[SetUpSteps]
public void SetUpSteps()
{
AddStep("set up working beatmap", () =>
{
Beatmap.Value.BeatmapInfo.OnlineID = 42;
});
AddStep("set up network requests", () =>
{
dummyAPI.HandleRequest = request =>
@@ -36,10 +39,19 @@ namespace osu.Game.Tests.Visual.Ranking
{
Tags =
[
new APITag { Id = 1, Name = "tech", Description = "Tests uncommon skills.", },
new APITag { Id = 2, Name = "alt", Description = "Colloquial term for maps which use rhythms that encourage the player to alternate notes. Typically distinct from burst or stream maps.", },
new APITag { Id = 3, Name = "aim", Description = "Category for difficulty relating to cursor movement.", },
new APITag { Id = 4, Name = "tap", Description = "Category for difficulty relating to tapping input.", },
new APITag { Id = 0, Name = "uncategorised tag", Description = "This probably isn't real but could be and should be handled.", },
new APITag { Id = 1, Name = "song representation/simple", Description = "Accessible and straightforward map design.", },
new APITag
{
Id = 2, Name = "style/clean",
Description = "Visually uncluttered and organised patterns, often involving few overlaps and equal visual spacing between objects.",
},
new APITag
{
Id = 3, Name = "aim/aim control", Description = "Patterns with velocity or direction changes which strongly go against a player's natural movement pattern.",
},
new APITag { Id = 4, Name = "tap/bursts", Description = "Patterns requiring continuous movement and alternating, typically 9 notes or less.", },
new APITag { Id = 5, Name = "style/mono-heavy", Description = "Features monos used in large amounts.", RulesetId = 1, },
]
}), 500);
return true;
@@ -67,19 +79,34 @@ namespace osu.Game.Tests.Visual.Ranking
return false;
};
});
AddStep("create control", () =>
AddStep("show for osu! beatmap", () =>
{
Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = new UserTagControl(Beatmap.Value.BeatmapInfo)
{
Width = 500,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
};
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.BeatmapInfo.OnlineID = 42;
Beatmap.Value = working;
recreateControl();
});
AddStep("show for taiko beatmap", () =>
{
var working = CreateWorkingBeatmap(new TaikoRuleset().RulesetInfo);
working.BeatmapInfo.OnlineID = 44;
Beatmap.Value = working;
recreateControl();
});
}
private void recreateControl()
{
Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = new UserTagControl(Beatmap.Value.BeatmapInfo)
{
Width = 700,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
};
}
}
}
@@ -1,11 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Overlays.Settings.Sections.Audio;
@@ -70,16 +73,54 @@ namespace osu.Game.Tests.Visual.Settings
AddStep("clear history", () => tracker.ClearHistory());
}
[Test]
public void TestRounding()
{
AddStep("set new score", () => statics.SetValue(Static.LastLocalUserScore, new ScoreInfo
{
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateHitEvents(0.6),
BeatmapInfo = Beatmap.Value.BeatmapInfo,
}));
checkButtonEnabled();
AddStep("click button", () => adjustControl.ChildrenOfType<Button>().Single().TriggerClick());
checkButtonDisabled();
AddAssert("global offset set correctly", () => localConfig.Get<double>(OsuSetting.AudioOffset), () => Is.EqualTo(-1));
}
[Test]
public void TestNegligibleChangeNotApplicable()
{
AddStep("set new score", () => statics.SetValue(Static.LastLocalUserScore, new ScoreInfo
{
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateHitEvents(0.5),
BeatmapInfo = Beatmap.Value.BeatmapInfo,
}));
checkButtonDisabled();
AddStep("adjust global offset", () => localConfig.SetValue(OsuSetting.AudioOffset, 50.0));
checkButtonEnabled();
AddStep("click button", () => adjustControl.ChildrenOfType<Button>().Single().TriggerClick());
checkButtonDisabled();
AddAssert("global offset set correctly", () => localConfig.Get<double>(OsuSetting.AudioOffset), () => Is.EqualTo(0));
AddStep("clear history", () => tracker.ClearHistory());
}
[Test]
public void TestBehaviour()
{
AddStep("set score with -20ms", () => setScore(-20));
AddAssert("suggested global offset is 20ms", () => adjustControl.SuggestedOffset.Value, () => Is.EqualTo(20));
checkButtonEnabled();
AddStep("clear history", () => tracker.ClearHistory());
checkButtonDisabled();
AddStep("set score with 40ms", () => setScore(40));
checkButtonEnabled();
AddAssert("suggested global offset is -40ms", () => adjustControl.SuggestedOffset.Value, () => Is.EqualTo(-40));
AddStep("clear history", () => tracker.ClearHistory());
checkButtonDisabled();
}
[Test]
@@ -111,6 +152,16 @@ namespace osu.Game.Tests.Visual.Settings
AddStep("clear history", () => tracker.ClearHistory());
}
private void checkButtonDisabled()
{
AddAssert("button is disabled", () => adjustControl.ChildrenOfType<Button>().Single().Enabled.Value, () => Is.False);
}
private void checkButtonEnabled()
{
AddAssert("button is enabled", () => adjustControl.ChildrenOfType<Button>().Single().Enabled.Value, () => Is.True);
}
private void setScore(double averageHitError)
{
statics.SetValue(Static.LastLocalUserScore, new ScoreInfo
@@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.SongSelect
foreach (var rulesetInfo in rulesets.AvailableRulesets)
{
var instance = rulesetInfo.CreateInstance();
var testBeatmap = createTestBeatmap(rulesetInfo);
var testBeatmap = CreateTestBeatmap(rulesetInfo);
beatmaps.Add(testBeatmap);
@@ -124,6 +124,12 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("reset mods", () => SelectedMods.SetDefault());
}
[Test]
public void TestTruncation()
{
selectBeatmap(CreateLongMetadata());
}
[Test]
public void TestNullBeatmap()
{
@@ -135,17 +141,11 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("check no info labels", () => !infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Any());
}
[Test]
public void TestTruncation()
{
selectBeatmap(createLongMetadata());
}
[Test]
public void TestBPMUpdates()
{
const double bpm = 120;
IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo);
IBeatmap beatmap = CreateTestBeatmap(new OsuRuleset().RulesetInfo);
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / bpm });
OsuModDoubleTime doubleTime = null!;
@@ -167,7 +167,7 @@ namespace osu.Game.Tests.Visual.SongSelect
[TestCase(120, 120.4, "DT", "180")]
public void TestVaryingBPM(double commonBpm, double otherBpm, string? mod, string expectedDisplay)
{
IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo);
IBeatmap beatmap = CreateTestBeatmap(new OsuRuleset().RulesetInfo);
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm });
beatmap.ControlPointInfo.Add(100, new TimingControlPoint { BeatLength = 60 * 1000 / otherBpm });
beatmap.ControlPointInfo.Add(200, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm });
@@ -191,7 +191,7 @@ namespace osu.Game.Tests.Visual.SongSelect
[TestCase]
public void TestLengthUpdates()
{
IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo);
IBeatmap beatmap = CreateTestBeatmap(new OsuRuleset().RulesetInfo);
double drain = beatmap.CalculateDrainLength();
beatmap.BeatmapInfo.Length = drain;
@@ -248,7 +248,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore);
}
private IBeatmap createTestBeatmap(RulesetInfo ruleset)
public static IBeatmap CreateTestBeatmap(RulesetInfo ruleset)
{
List<HitObject> objects = new List<HitObject>();
for (double i = 0; i < 50000; i += 1000)
@@ -274,7 +274,7 @@ namespace osu.Game.Tests.Visual.SongSelect
};
}
private IBeatmap createLongMetadata()
public static IBeatmap CreateLongMetadata()
{
return new Beatmap
{
@@ -43,6 +43,8 @@ namespace osu.Game.Tests.Visual.SongSelect
private BeatmapManager beatmapManager = null!;
private PlaySongSelect songSelect = null!;
private LeaderboardManager leaderboardManager = null!;
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
@@ -51,6 +53,8 @@ namespace osu.Game.Tests.Visual.SongSelect
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, API));
dependencies.CacheAs<Screens.Select.SongSelect>(songSelect = new PlaySongSelect());
dependencies.Cache(leaderboardManager = new LeaderboardManager());
Dependencies.Cache(Realm);
return dependencies;
@@ -60,6 +64,7 @@ namespace osu.Game.Tests.Visual.SongSelect
private void load()
{
LoadComponent(songSelect);
LoadComponent(leaderboardManager);
}
public TestSceneBeatmapLeaderboard()
@@ -112,6 +117,27 @@ namespace osu.Game.Tests.Visual.SongSelect
checkDisplayedCount(0);
}
[Test]
public void TestLocalScoresDisplayWorksWhenStartingOffline()
{
BeatmapInfo beatmapInfo = null!;
AddStep("Log out", () => API.Logout());
AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Local);
AddStep(@"Set beatmap", () =>
{
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First();
leaderboard.BeatmapInfo = beatmapInfo;
});
clearScores();
importMoreScores(() => beatmapInfo);
checkDisplayedCount(10);
}
[Test]
public void TestLocalScoresDisplayOnBeatmapEdit()
{
@@ -180,8 +206,8 @@ namespace osu.Game.Tests.Visual.SongSelect
public void TestGlobalScoresDisplay()
{
AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Global);
AddStep(@"New Scores", () => leaderboard.SetScores(generateSampleScores(new BeatmapInfo())));
AddStep(@"New Scores with teams", () => leaderboard.SetScores(generateSampleScores(new BeatmapInfo()).Select(s =>
AddStep(@"New Scores", () => leaderboard.SetScores(GenerateSampleScores(new BeatmapInfo())));
AddStep(@"New Scores with teams", () => leaderboard.SetScores(GenerateSampleScores(new BeatmapInfo()).Select(s =>
{
s.User.Team = new APITeam();
return s;
@@ -286,7 +312,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
AddStep(@"Import new scores", () =>
{
foreach (var score in generateSampleScores(beatmapInfo()))
foreach (var score in GenerateSampleScores(beatmapInfo()))
scoreManager.Import(score);
});
}
@@ -302,7 +328,7 @@ namespace osu.Game.Tests.Visual.SongSelect
private void checkStoredCount(int expected) =>
AddUntilStep($"Total scores stored is {expected}", () => Realm.Run(r => r.All<ScoreInfo>().Count(s => !s.DeletePending)), () => Is.EqualTo(expected));
private static ScoreInfo[] generateSampleScores(BeatmapInfo beatmapInfo)
public static ScoreInfo[] GenerateSampleScores(BeatmapInfo beatmapInfo)
{
return new[]
{
@@ -316,7 +342,6 @@ namespace osu.Game.Tests.Visual.SongSelect
Mods = new Mod[]
{
new OsuModHidden(),
new OsuModHardRock(),
new OsuModFlashlight
{
FollowDelay = { Value = 200 },
@@ -0,0 +1,271 @@
// 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.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Tests.Resources;
using osuTK.Input;
using Realms;
namespace osu.Game.Tests.Visual.SongSelect
{
public partial class TestSceneCollectionDropdown : OsuManualInputManagerTestScene
{
private BeatmapManager beatmapManager = null!;
private CollectionDropdown dropdown = null!;
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
[BackgroundDependencyLoader]
private void load(GameHost host)
{
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
}
[SetUp]
public void SetUp() => Schedule(() =>
{
writeAndRefresh(r => r.RemoveAll<BeatmapCollection>());
Child = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = dropdown = new CollectionDropdown
{
Width = 300,
Y = 100,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
};
});
[Test]
public void TestEmptyCollectionFilterContainsAllBeatmaps()
{
assertCollectionDropdownContains("All beatmaps");
assertCollectionHeaderDisplays("All beatmaps");
}
[Test]
public void TestCollectionAddedToDropdown()
{
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2"))));
assertCollectionDropdownContains("1");
assertCollectionDropdownContains("2");
}
[Test]
public void TestCollectionsCleared()
{
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2"))));
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "3"))));
AddAssert("check count 5", () => dropdown.ChildrenOfType<CollectionDropdown>().Single().ChildrenOfType<Menu.DrawableMenuItem>().Count(), () => Is.EqualTo(5));
AddStep("delete all collections", () => writeAndRefresh(r => r.RemoveAll<BeatmapCollection>()));
AddAssert("check count 2", () => dropdown.ChildrenOfType<CollectionDropdown>().Single().ChildrenOfType<Menu.DrawableMenuItem>().Count(), () => Is.EqualTo(2));
}
[Test]
public void TestCollectionRemovedFromDropdown()
{
BeatmapCollection first = null!;
AddStep("add collection", () => writeAndRefresh(r => r.Add(first = new BeatmapCollection(name: "1"))));
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2"))));
AddStep("remove collection", () => writeAndRefresh(r => r.Remove(first)));
assertCollectionDropdownContains("1", false);
assertCollectionDropdownContains("2");
}
[Test]
public void TestCollectionRenamed()
{
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
assertCollectionDropdownContains("1");
AddStep("select collection", () => dropdown.Current.Value = dropdown.ItemSource.ElementAt(1));
addExpandHeaderStep();
AddStep("change name", () => writeAndRefresh(_ => getFirstCollection().Name = "First"));
assertCollectionDropdownContains("First");
assertCollectionHeaderDisplays("First");
}
[Test]
public void TestAllBeatmapFilterDoesNotHaveAddButton()
{
addExpandHeaderStep();
AddStep("hover all beatmaps", () => InputManager.MoveMouseTo(getAddOrRemoveButton(0)));
AddAssert("'All beatmaps' filter does not have add button", () => !getAddOrRemoveButton(0).IsPresent);
}
[Test]
public void TestCollectionFilterHasAddButton()
{
addExpandHeaderStep();
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
assertCollectionDropdownContains("1");
AddStep("hover collection", () => InputManager.MoveMouseTo(getAddOrRemoveButton(1)));
AddAssert("collection has add button", () => getAddOrRemoveButton(1).IsPresent);
}
[Test]
public void TestButtonDisabledAndEnabledWithBeatmapChanges()
{
addExpandHeaderStep();
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
assertCollectionDropdownContains("1");
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
AddAssert("button enabled", () => getAddOrRemoveButton(1).Enabled.Value);
AddStep("set dummy beatmap", () => Beatmap.SetDefault());
AddAssert("button disabled", () => !getAddOrRemoveButton(1).Enabled.Value);
}
[Test]
public void TestButtonChangesWhenAddedAndRemovedFromCollection()
{
addExpandHeaderStep();
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
assertCollectionDropdownContains("1");
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
AddStep("add beatmap to collection", () => writeAndRefresh(r => getFirstCollection().BeatmapMD5Hashes.Add(Beatmap.Value.BeatmapInfo.MD5Hash)));
assertFirstButtonIs(FontAwesome.Solid.MinusSquare);
AddStep("remove beatmap from collection", () => writeAndRefresh(r => getFirstCollection().BeatmapMD5Hashes.Clear()));
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
}
[Test]
public void TestButtonAddsAndRemovesBeatmap()
{
addExpandHeaderStep();
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
assertCollectionDropdownContains("1");
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
addClickAddOrRemoveButtonStep(1);
AddAssert("collection contains beatmap", () => getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
assertFirstButtonIs(FontAwesome.Solid.MinusSquare);
addClickAddOrRemoveButtonStep(1);
AddAssert("collection does not contain beatmap", () => !getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
}
[Test]
public void TestManageCollectionsFilterIsNotSelected()
{
bool received = false;
addExpandHeaderStep();
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1", new List<string> { "abc" }))));
assertCollectionDropdownContains("1");
AddStep("select collection", () =>
{
InputManager.MoveMouseTo(getCollectionDropdownItemAt(1));
InputManager.Click(MouseButton.Left);
});
addExpandHeaderStep();
AddStep("watch for filter requests", () =>
{
received = false;
dropdown.ChildrenOfType<CollectionDropdown>().First().RequestFilter = () => received = true;
});
AddStep("click manage collections filter", () =>
{
int lastItemIndex = dropdown.ChildrenOfType<CollectionDropdown>().Single().Items.Count() - 1;
InputManager.MoveMouseTo(getCollectionDropdownItemAt(lastItemIndex));
InputManager.Click(MouseButton.Left);
});
AddAssert("collection filter still selected", () => dropdown.Current.Value.CollectionName == "1");
AddAssert("filter request not fired", () => !received);
}
private void writeAndRefresh(Action<Realm> action) => Realm.Write(r =>
{
action(r);
r.Refresh();
});
private BeatmapCollection getFirstCollection() => Realm.Run(r => r.All<BeatmapCollection>().First());
private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true)
=> AddUntilStep($"collection dropdown header displays '{collectionName}'",
() => shouldDisplay == dropdown.ChildrenOfType<CollectionDropdown.OsuDropdownHeader>().Any(h => h.ChildrenOfType<SpriteText>().Any(t => t.Text == collectionName)));
private void assertFirstButtonIs(IconUsage icon) => AddUntilStep($"button is {icon.Icon.ToString()}", () => getAddOrRemoveButton(1).Icon.Equals(icon));
private void assertCollectionDropdownContains(string collectionName, bool shouldContain = true) =>
AddUntilStep($"collection dropdown {(shouldContain ? "contains" : "does not contain")} '{collectionName}'",
// A bit of a roundabout way of going about this, see: https://github.com/ppy/osu-framework/issues/3871 + https://github.com/ppy/osu-framework/issues/3872
() => shouldContain == dropdown.ChildrenOfType<Menu.DrawableMenuItem>().Any(i => i.ChildrenOfType<CompositeDrawable>().OfType<IHasText>().First().Text == collectionName));
private IconButton getAddOrRemoveButton(int index)
=> getCollectionDropdownItemAt(index).ChildrenOfType<IconButton>().Single();
private void addExpandHeaderStep() => AddStep("expand header", () =>
{
InputManager.MoveMouseTo(dropdown.ChildrenOfType<CollectionDropdown.OsuDropdownHeader>().Single());
InputManager.Click(MouseButton.Left);
});
private void addClickAddOrRemoveButtonStep(int index) => AddStep("click add or remove button", () =>
{
InputManager.MoveMouseTo(getAddOrRemoveButton(index));
InputManager.Click(MouseButton.Left);
});
private Menu.DrawableMenuItem getCollectionDropdownItemAt(int index)
{
// todo: we should be able to use Items, but apparently that's not guaranteed to be ordered... see: https://github.com/ppy/osu-framework/pull/6079
CollectionFilterMenuItem item = dropdown.ChildrenOfType<CollectionDropdown>().Single().ItemSource.ElementAt(index);
return dropdown.ChildrenOfType<Menu.DrawableMenuItem>().Single(i => i.Item.Text.Value == item.CollectionName);
}
}
}
@@ -1,57 +1,18 @@
// 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.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osu.Game.Screens.Select;
using osu.Game.Tests.Resources;
using osuTK.Input;
using Realms;
namespace osu.Game.Tests.Visual.SongSelect
{
public partial class TestSceneFilterControl : OsuManualInputManagerTestScene
{
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
private BeatmapManager beatmapManager = null!;
private FilterControl control = null!;
[BackgroundDependencyLoader]
private void load(GameHost host)
{
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
base.Content.AddRange(new Drawable[]
{
Content
});
}
[SetUp]
public void SetUp() => Schedule(() =>
{
writeAndRefresh(r => r.RemoveAll<BeatmapCollection>());
Child = control = new FilterControl
Child = new FilterControl
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -59,216 +20,5 @@ namespace osu.Game.Tests.Visual.SongSelect
Height = FilterControl.HEIGHT,
};
});
[Test]
public void TestEmptyCollectionFilterContainsAllBeatmaps()
{
assertCollectionDropdownContains("All beatmaps");
assertCollectionHeaderDisplays("All beatmaps");
}
[Test]
public void TestCollectionAddedToDropdown()
{
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2"))));
assertCollectionDropdownContains("1");
assertCollectionDropdownContains("2");
}
[Test]
public void TestCollectionsCleared()
{
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2"))));
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "3"))));
AddAssert("check count 5", () => control.ChildrenOfType<CollectionDropdown>().Single().ChildrenOfType<Menu.DrawableMenuItem>().Count(), () => Is.EqualTo(5));
AddStep("delete all collections", () => writeAndRefresh(r => r.RemoveAll<BeatmapCollection>()));
AddAssert("check count 2", () => control.ChildrenOfType<CollectionDropdown>().Single().ChildrenOfType<Menu.DrawableMenuItem>().Count(), () => Is.EqualTo(2));
}
[Test]
public void TestCollectionRemovedFromDropdown()
{
BeatmapCollection first = null!;
AddStep("add collection", () => writeAndRefresh(r => r.Add(first = new BeatmapCollection(name: "1"))));
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2"))));
AddStep("remove collection", () => writeAndRefresh(r => r.Remove(first)));
assertCollectionDropdownContains("1", false);
assertCollectionDropdownContains("2");
}
[Test]
public void TestCollectionRenamed()
{
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
assertCollectionDropdownContains("1");
AddStep("select collection", () =>
{
var dropdown = control.ChildrenOfType<CollectionDropdown>().Single();
dropdown.Current.Value = dropdown.ItemSource.ElementAt(1);
});
addExpandHeaderStep();
AddStep("change name", () => writeAndRefresh(_ => getFirstCollection().Name = "First"));
assertCollectionDropdownContains("First");
assertCollectionHeaderDisplays("First");
}
[Test]
public void TestAllBeatmapFilterDoesNotHaveAddButton()
{
addExpandHeaderStep();
AddStep("hover all beatmaps", () => InputManager.MoveMouseTo(getAddOrRemoveButton(0)));
AddAssert("'All beatmaps' filter does not have add button", () => !getAddOrRemoveButton(0).IsPresent);
}
[Test]
public void TestCollectionFilterHasAddButton()
{
addExpandHeaderStep();
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
assertCollectionDropdownContains("1");
AddStep("hover collection", () => InputManager.MoveMouseTo(getAddOrRemoveButton(1)));
AddAssert("collection has add button", () => getAddOrRemoveButton(1).IsPresent);
}
[Test]
public void TestButtonDisabledAndEnabledWithBeatmapChanges()
{
addExpandHeaderStep();
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
assertCollectionDropdownContains("1");
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
AddAssert("button enabled", () => getAddOrRemoveButton(1).Enabled.Value);
AddStep("set dummy beatmap", () => Beatmap.SetDefault());
AddAssert("button disabled", () => !getAddOrRemoveButton(1).Enabled.Value);
}
[Test]
public void TestButtonChangesWhenAddedAndRemovedFromCollection()
{
addExpandHeaderStep();
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
assertCollectionDropdownContains("1");
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
AddStep("add beatmap to collection", () => writeAndRefresh(r => getFirstCollection().BeatmapMD5Hashes.Add(Beatmap.Value.BeatmapInfo.MD5Hash)));
assertFirstButtonIs(FontAwesome.Solid.MinusSquare);
AddStep("remove beatmap from collection", () => writeAndRefresh(r => getFirstCollection().BeatmapMD5Hashes.Clear()));
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
}
[Test]
public void TestButtonAddsAndRemovesBeatmap()
{
addExpandHeaderStep();
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
assertCollectionDropdownContains("1");
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
addClickAddOrRemoveButtonStep(1);
AddAssert("collection contains beatmap", () => getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
assertFirstButtonIs(FontAwesome.Solid.MinusSquare);
addClickAddOrRemoveButtonStep(1);
AddAssert("collection does not contain beatmap", () => !getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
}
[Test]
public void TestManageCollectionsFilterIsNotSelected()
{
bool received = false;
addExpandHeaderStep();
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1", new List<string> { "abc" }))));
assertCollectionDropdownContains("1");
AddStep("select collection", () =>
{
InputManager.MoveMouseTo(getCollectionDropdownItemAt(1));
InputManager.Click(MouseButton.Left);
});
addExpandHeaderStep();
AddStep("watch for filter requests", () =>
{
received = false;
control.ChildrenOfType<CollectionDropdown>().First().RequestFilter = () => received = true;
});
AddStep("click manage collections filter", () =>
{
int lastItemIndex = control.ChildrenOfType<CollectionDropdown>().Single().Items.Count() - 1;
InputManager.MoveMouseTo(getCollectionDropdownItemAt(lastItemIndex));
InputManager.Click(MouseButton.Left);
});
AddAssert("collection filter still selected", () => control.CreateCriteria().CollectionBeatmapMD5Hashes?.Any() == true);
AddAssert("filter request not fired", () => !received);
}
private void writeAndRefresh(Action<Realm> action) => Realm.Write(r =>
{
action(r);
r.Refresh();
});
private BeatmapCollection getFirstCollection() => Realm.Run(r => r.All<BeatmapCollection>().First());
private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true)
=> AddUntilStep($"collection dropdown header displays '{collectionName}'",
() => shouldDisplay == (control.ChildrenOfType<CollectionDropdown.CollectionDropdownHeader>().Single().ChildrenOfType<SpriteText>().First().Text == collectionName));
private void assertFirstButtonIs(IconUsage icon) => AddUntilStep($"button is {icon.Icon.ToString()}", () => getAddOrRemoveButton(1).Icon.Equals(icon));
private void assertCollectionDropdownContains(string collectionName, bool shouldContain = true) =>
AddUntilStep($"collection dropdown {(shouldContain ? "contains" : "does not contain")} '{collectionName}'",
// A bit of a roundabout way of going about this, see: https://github.com/ppy/osu-framework/issues/3871 + https://github.com/ppy/osu-framework/issues/3872
() => shouldContain == control.ChildrenOfType<Menu.DrawableMenuItem>().Any(i => i.ChildrenOfType<CompositeDrawable>().OfType<IHasText>().First().Text == collectionName));
private IconButton getAddOrRemoveButton(int index)
=> getCollectionDropdownItemAt(index).ChildrenOfType<IconButton>().Single();
private void addExpandHeaderStep() => AddStep("expand header", () =>
{
InputManager.MoveMouseTo(control.ChildrenOfType<CollectionDropdown.CollectionDropdownHeader>().Single());
InputManager.Click(MouseButton.Left);
});
private void addClickAddOrRemoveButtonStep(int index) => AddStep("click add or remove button", () =>
{
InputManager.MoveMouseTo(getAddOrRemoveButton(index));
InputManager.Click(MouseButton.Left);
});
private Menu.DrawableMenuItem getCollectionDropdownItemAt(int index)
{
// todo: we should be able to use Items, but apparently that's not guaranteed to be ordered... see: https://github.com/ppy/osu-framework/pull/6079
CollectionFilterMenuItem item = control.ChildrenOfType<CollectionDropdown>().Single().ItemSource.ElementAt(index);
return control.ChildrenOfType<Menu.DrawableMenuItem>().Single(i => i.Item.Text.Value == item.CollectionName);
}
}
}
@@ -20,7 +20,7 @@ using osu.Game.Tests.Resources;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Collections
namespace osu.Game.Tests.Visual.SongSelect
{
public partial class TestSceneManageCollectionsDialog : OsuManualInputManagerTestScene
{
@@ -16,6 +16,7 @@ using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Carousel;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays;
using osu.Game.Screens.Select;
@@ -27,9 +28,9 @@ using osuTK.Graphics;
using osuTK.Input;
using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel;
namespace osu.Game.Tests.Visual.SongSelect
namespace osu.Game.Tests.Visual.SongSelectV2
{
public abstract partial class BeatmapCarouselV2TestScene : OsuManualInputManagerTestScene
public abstract partial class BeatmapCarouselTestScene : OsuManualInputManagerTestScene
{
protected readonly BindableList<BeatmapSetInfo> BeatmapSets = new BindableList<BeatmapSetInfo>();
@@ -47,7 +48,7 @@ namespace osu.Game.Tests.Visual.SongSelect
private int beatmapCount;
protected BeatmapCarouselV2TestScene()
protected BeatmapCarouselTestScene()
{
store = new TestBeatmapStore
{
@@ -96,6 +97,8 @@ namespace osu.Game.Tests.Visual.SongSelect
{
Carousel = new BeatmapCarousel
{
BleedTop = 50,
BleedBottom = 50,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 800,
@@ -189,7 +192,7 @@ namespace osu.Game.Tests.Visual.SongSelect
.Where(p => ((ICarouselPanel)p).Item?.IsVisible == true)
.OrderBy(p => p.Y)
.ElementAt(index)
.ChildrenOfType<PanelBase>().Single()
.ChildrenOfType<Panel>().Single()
.TriggerClick();
});
}
@@ -4,7 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Testing;
using osu.Game.Graphics.Cursor;
using osu.Game.Overlays;
@@ -20,33 +20,32 @@ namespace osu.Game.Tests.Visual.SongSelectV2
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding(10),
};
private Container? resizeContainer;
private float relativeWidth;
protected virtual Anchor ComponentAnchor => Anchor.TopLeft;
protected virtual float InitialRelativeWidth => 0.5f;
[BackgroundDependencyLoader]
private void load()
{
base.Content.Child = resizeContainer = new Container
base.Content.Child = new PopoverContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding(10),
Width = relativeWidth,
Children = new Drawable[]
RelativeSizeAxes = Axes.Both,
Child = resizeContainer = new Container
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourProvider.Background5,
},
Content
Anchor = ComponentAnchor,
Origin = ComponentAnchor,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Width = relativeWidth,
Child = Content
}
};
AddSliderStep("change relative width", 0, 1f, 1f, v =>
AddSliderStep("change relative width", 0, 1f, InitialRelativeWidth, v =>
{
if (resizeContainer != null)
resizeContainer.Width = v;
@@ -55,6 +54,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2
});
}
protected override void LoadComplete()
{
base.LoadComplete();
ChangeBackgroundColour(ColourProvider.Background6);
}
[SetUpSteps]
public virtual void SetUpSteps()
{
@@ -10,13 +10,13 @@ using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.SongSelect
namespace osu.Game.Tests.Visual.SongSelectV2
{
/// <summary>
/// Covers common steps which can be used for manual testing.
/// </summary>
[TestFixture]
public partial class TestSceneBeatmapCarouselV2 : BeatmapCarouselV2TestScene
public partial class TestSceneBeatmapCarousel : BeatmapCarouselTestScene
{
[Test]
[Explicit]
@@ -9,10 +9,10 @@ using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
namespace osu.Game.Tests.Visual.SongSelect
namespace osu.Game.Tests.Visual.SongSelectV2
{
[TestFixture]
public partial class TestSceneBeatmapCarouselV2ArtistGrouping : BeatmapCarouselV2TestScene
public partial class TestSceneBeatmapCarouselArtistGrouping : BeatmapCarouselTestScene
{
[SetUpSteps]
public void SetUpSteps()
@@ -5,15 +5,16 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Carousel;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
using osuTK;
namespace osu.Game.Tests.Visual.SongSelect
namespace osu.Game.Tests.Visual.SongSelectV2
{
[TestFixture]
public partial class TestSceneBeatmapCarouselV2DifficultyGrouping : BeatmapCarouselV2TestScene
public partial class TestSceneBeatmapCarouselDifficultyGrouping : BeatmapCarouselTestScene
{
[SetUpSteps]
public void SetUpSteps()
@@ -5,16 +5,17 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Carousel;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.SongSelect
namespace osu.Game.Tests.Visual.SongSelectV2
{
[TestFixture]
public partial class TestSceneBeatmapCarouselV2NoGrouping : BeatmapCarouselV2TestScene
public partial class TestSceneBeatmapCarouselNoGrouping : BeatmapCarouselTestScene
{
[SetUpSteps]
public void SetUpSteps()
@@ -8,10 +8,10 @@ using osu.Framework.Testing;
using osu.Game.Screens.Select;
using osu.Game.Screens.SelectV2;
namespace osu.Game.Tests.Visual.SongSelect
namespace osu.Game.Tests.Visual.SongSelectV2
{
[TestFixture]
public partial class TestSceneBeatmapCarouselV2Scrolling : BeatmapCarouselV2TestScene
public partial class TestSceneBeatmapCarouselScrolling : BeatmapCarouselTestScene
{
[SetUpSteps]
public void SetUpSteps()
@@ -1,83 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Visual.UserInterface;
using osuTK;
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneBeatmapCarouselV2GroupPanel : ThemeComparisonTestScene
{
public TestSceneBeatmapCarouselV2GroupPanel()
: base(false)
{
}
protected override Drawable CreateContent()
{
return new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0f, 5f),
Children = new Drawable[]
{
new PanelGroup
{
Item = new CarouselItem(new GroupDefinition('A', "Group A"))
},
new PanelGroup
{
Item = new CarouselItem(new GroupDefinition('A', "Group A")),
KeyboardSelected = { Value = true }
},
new PanelGroup
{
Item = new CarouselItem(new GroupDefinition('A', "Group A")),
Expanded = { Value = true }
},
new PanelGroup
{
Item = new CarouselItem(new GroupDefinition('A', "Group A")),
KeyboardSelected = { Value = true },
Expanded = { Value = true }
},
new PanelGroupStarDifficulty
{
Item = new CarouselItem(new GroupDefinition(1, "1"))
},
new PanelGroupStarDifficulty
{
Item = new CarouselItem(new GroupDefinition(3, "3")),
Expanded = { Value = true }
},
new PanelGroupStarDifficulty
{
Item = new CarouselItem(new GroupDefinition(5, "5")),
},
new PanelGroupStarDifficulty
{
Item = new CarouselItem(new GroupDefinition(7, "7")),
Expanded = { Value = true }
},
new PanelGroupStarDifficulty
{
Item = new CarouselItem(new GroupDefinition(8, "8")),
},
new PanelGroupStarDifficulty
{
Item = new CarouselItem(new GroupDefinition(9, "9")),
Expanded = { Value = true }
},
}
};
}
}
}
@@ -1,213 +0,0 @@
// 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.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Screens.Select;
using osu.Game.Screens.SelectV2;
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneBeatmapInfoWedge : SongSelectComponentsTestScene
{
private RulesetStore rulesets = null!;
private TestBeatmapInfoWedgeV2 infoWedge = null!;
private readonly List<IBeatmap> beatmaps = new List<IBeatmap>();
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets)
{
this.rulesets = rulesets;
}
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("reset mods", () => SelectedMods.SetDefault());
}
protected override void LoadComplete()
{
base.LoadComplete();
AddRange(new Drawable[]
{
// This exists only to make the wedge more visible in the test scene
new Box
{
Y = -20,
Colour = Colour4.Cornsilk.Darken(0.2f),
Height = BeatmapInfoWedgeV2.WEDGE_HEIGHT + 40,
Width = 0.65f,
RelativeSizeAxes = Axes.X,
Margin = new MarginPadding { Top = 20, Left = -10 }
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = 20 },
Child = infoWedge = new TestBeatmapInfoWedgeV2
{
Width = 0.6f,
RelativeSizeAxes = Axes.X,
},
}
});
AddSliderStep("change star difficulty", 0, 11.9, 5.55, v =>
{
foreach (var hasCurrentValue in infoWedge.ChildrenOfType<IHasCurrentValue<StarDifficulty>>())
hasCurrentValue.Current.Value = new StarDifficulty(v, 0);
});
}
[Test]
public void TestRulesetChange()
{
selectBeatmap(Beatmap.Value.Beatmap);
AddWaitStep("wait for select", 3);
foreach (var rulesetInfo in rulesets.AvailableRulesets)
{
var instance = rulesetInfo.CreateInstance();
var testBeatmap = createTestBeatmap(rulesetInfo);
beatmaps.Add(testBeatmap);
setRuleset(rulesetInfo);
selectBeatmap(testBeatmap);
testBeatmapLabels(instance);
}
}
[Test]
public void TestWedgeVisibility()
{
AddStep("hide", () => { infoWedge.Hide(); });
AddWaitStep("wait for hide", 3);
AddAssert("check visibility", () => infoWedge.Alpha == 0);
AddStep("show", () => { infoWedge.Show(); });
AddWaitStep("wait for show", 1);
AddAssert("check visibility", () => infoWedge.Alpha > 0);
}
private void testBeatmapLabels(Ruleset ruleset)
{
AddAssert("check title", () => infoWedge.Info!.TitleLabel.Current.Value == $"{ruleset.ShortName}Title");
AddAssert("check artist", () => infoWedge.Info!.ArtistLabel.Current.Value == $"{ruleset.ShortName}Artist");
}
[Test]
public void TestTruncation()
{
selectBeatmap(createLongMetadata());
}
[Test]
public void TestNullBeatmapWithBackground()
{
selectBeatmap(null);
AddAssert("check default title", () => infoWedge.Info!.TitleLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Title);
AddAssert("check default artist", () => infoWedge.Info!.ArtistLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Artist);
AddAssert("check no info labels", () => !infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Any());
}
private void setRuleset(RulesetInfo rulesetInfo)
{
Container? containerBefore = null;
AddStep("set ruleset", () =>
{
// wedge content is only refreshed if the ruleset changes, so only wait for load in that case.
if (!rulesetInfo.Equals(Ruleset.Value))
containerBefore = infoWedge.DisplayedContent;
Ruleset.Value = rulesetInfo;
});
AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore);
}
private void selectBeatmap(IBeatmap? b)
{
Container? containerBefore = null;
AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () =>
{
containerBefore = infoWedge.DisplayedContent;
infoWedge.Beatmap = Beatmap.Value = b == null ? Beatmap.Default : CreateWorkingBeatmap(b);
infoWedge.Show();
});
AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore);
}
private IBeatmap createTestBeatmap(RulesetInfo ruleset)
{
List<HitObject> objects = new List<HitObject>();
for (double i = 0; i < 50000; i += 1000)
objects.Add(new TestHitObject { StartTime = i });
return new Beatmap
{
BeatmapInfo = new BeatmapInfo
{
Metadata = new BeatmapMetadata
{
Author = { Username = $"{ruleset.ShortName}Author" },
Artist = $"{ruleset.ShortName}Artist",
Source = $"{ruleset.ShortName}Source",
Title = $"{ruleset.ShortName}Title"
},
Ruleset = ruleset,
StarRating = 6,
DifficultyName = $"{ruleset.ShortName}Version",
Difficulty = new BeatmapDifficulty()
},
HitObjects = objects
};
}
private IBeatmap createLongMetadata()
{
return new Beatmap
{
BeatmapInfo = new BeatmapInfo
{
Metadata = new BeatmapMetadata
{
Author = { Username = "WWWWWWWWWWWWWWW" },
Artist = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Artist",
Source = "Verrrrry long Source",
Title = "Verrrrry long Title"
},
DifficultyName = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Version",
Status = BeatmapOnlineStatus.Graveyard,
},
};
}
private partial class TestBeatmapInfoWedgeV2 : BeatmapInfoWedgeV2
{
public new Container? DisplayedContent => base.DisplayedContent;
public new WedgeInfoText? Info => base.Info;
}
private class TestHitObject : ConvertHitObject;
}
}
@@ -0,0 +1,272 @@
// 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.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Tests.Resources;
using osuTK.Input;
using Realms;
using CollectionDropdown = osu.Game.Screens.SelectV2.CollectionDropdown;
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneCollectionDropdown : OsuManualInputManagerTestScene
{
private BeatmapManager beatmapManager = null!;
private CollectionDropdown dropdown = null!;
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
[BackgroundDependencyLoader]
private void load(GameHost host)
{
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
}
[SetUp]
public void SetUp() => Schedule(() =>
{
writeAndRefresh(r => r.RemoveAll<BeatmapCollection>());
Child = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = dropdown = new CollectionDropdown
{
Width = 300,
Y = 100,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
};
});
[Test]
public void TestEmptyCollectionFilterContainsAllBeatmaps()
{
assertCollectionDropdownContains("All beatmaps");
assertCollectionHeaderDisplays("All beatmaps");
}
[Test]
public void TestCollectionAddedToDropdown()
{
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2"))));
assertCollectionDropdownContains("1");
assertCollectionDropdownContains("2");
}
[Test]
public void TestCollectionsCleared()
{
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2"))));
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "3"))));
AddAssert("check count 5", () => dropdown.ChildrenOfType<CollectionDropdown>().Single().ChildrenOfType<Menu.DrawableMenuItem>().Count(), () => Is.EqualTo(5));
AddStep("delete all collections", () => writeAndRefresh(r => r.RemoveAll<BeatmapCollection>()));
AddAssert("check count 2", () => dropdown.ChildrenOfType<CollectionDropdown>().Single().ChildrenOfType<Menu.DrawableMenuItem>().Count(), () => Is.EqualTo(2));
}
[Test]
public void TestCollectionRemovedFromDropdown()
{
BeatmapCollection first = null!;
AddStep("add collection", () => writeAndRefresh(r => r.Add(first = new BeatmapCollection(name: "1"))));
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2"))));
AddStep("remove collection", () => writeAndRefresh(r => r.Remove(first)));
assertCollectionDropdownContains("1", false);
assertCollectionDropdownContains("2");
}
[Test]
public void TestCollectionRenamed()
{
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
assertCollectionDropdownContains("1");
AddStep("select collection", () => dropdown.Current.Value = dropdown.ItemSource.ElementAt(1));
addExpandHeaderStep();
AddStep("change name", () => writeAndRefresh(_ => getFirstCollection().Name = "First"));
assertCollectionDropdownContains("First");
assertCollectionHeaderDisplays("First");
}
[Test]
public void TestAllBeatmapFilterDoesNotHaveAddButton()
{
addExpandHeaderStep();
AddStep("hover all beatmaps", () => InputManager.MoveMouseTo(getAddOrRemoveButton(0)));
AddAssert("'All beatmaps' filter does not have add button", () => !getAddOrRemoveButton(0).IsPresent);
}
[Test]
public void TestCollectionFilterHasAddButton()
{
addExpandHeaderStep();
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
assertCollectionDropdownContains("1");
AddStep("hover collection", () => InputManager.MoveMouseTo(getAddOrRemoveButton(1)));
AddAssert("collection has add button", () => getAddOrRemoveButton(1).IsPresent);
}
[Test]
public void TestButtonDisabledAndEnabledWithBeatmapChanges()
{
addExpandHeaderStep();
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
assertCollectionDropdownContains("1");
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
AddAssert("button enabled", () => getAddOrRemoveButton(1).Enabled.Value);
AddStep("set dummy beatmap", () => Beatmap.SetDefault());
AddAssert("button disabled", () => !getAddOrRemoveButton(1).Enabled.Value);
}
[Test]
public void TestButtonChangesWhenAddedAndRemovedFromCollection()
{
addExpandHeaderStep();
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
assertCollectionDropdownContains("1");
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
AddStep("add beatmap to collection", () => writeAndRefresh(r => getFirstCollection().BeatmapMD5Hashes.Add(Beatmap.Value.BeatmapInfo.MD5Hash)));
assertFirstButtonIs(FontAwesome.Solid.MinusSquare);
AddStep("remove beatmap from collection", () => writeAndRefresh(r => getFirstCollection().BeatmapMD5Hashes.Clear()));
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
}
[Test]
public void TestButtonAddsAndRemovesBeatmap()
{
addExpandHeaderStep();
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
assertCollectionDropdownContains("1");
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
addClickAddOrRemoveButtonStep(1);
AddAssert("collection contains beatmap", () => getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
assertFirstButtonIs(FontAwesome.Solid.MinusSquare);
addClickAddOrRemoveButtonStep(1);
AddAssert("collection does not contain beatmap", () => !getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
}
[Test]
public void TestManageCollectionsFilterIsNotSelected()
{
bool received = false;
addExpandHeaderStep();
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1", new List<string> { "abc" }))));
assertCollectionDropdownContains("1");
AddStep("select collection", () =>
{
InputManager.MoveMouseTo(getCollectionDropdownItemAt(1));
InputManager.Click(MouseButton.Left);
});
addExpandHeaderStep();
AddStep("watch for filter requests", () =>
{
received = false;
dropdown.ChildrenOfType<CollectionDropdown>().First().RequestFilter = () => received = true;
});
AddStep("click manage collections filter", () =>
{
int lastItemIndex = dropdown.ChildrenOfType<CollectionDropdown>().Single().Items.Count() - 1;
InputManager.MoveMouseTo(getCollectionDropdownItemAt(lastItemIndex));
InputManager.Click(MouseButton.Left);
});
AddAssert("collection filter still selected", () => dropdown.Current.Value.CollectionName == "1");
AddAssert("filter request not fired", () => !received);
}
private void writeAndRefresh(Action<Realm> action) => Realm.Write(r =>
{
action(r);
r.Refresh();
});
private BeatmapCollection getFirstCollection() => Realm.Run(r => r.All<BeatmapCollection>().First());
private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true)
=> AddUntilStep($"collection dropdown header displays '{collectionName}'",
() => shouldDisplay == dropdown.ChildrenOfType<CollectionDropdown.ShearedDropdownHeader>().Any(h => h.ChildrenOfType<SpriteText>().Any(t => t.Text == collectionName)));
private void assertFirstButtonIs(IconUsage icon) => AddUntilStep($"button is {icon.Icon.ToString()}", () => getAddOrRemoveButton(1).Icon.Equals(icon));
private void assertCollectionDropdownContains(string collectionName, bool shouldContain = true) =>
AddUntilStep($"collection dropdown {(shouldContain ? "contains" : "does not contain")} '{collectionName}'",
// A bit of a roundabout way of going about this, see: https://github.com/ppy/osu-framework/issues/3871 + https://github.com/ppy/osu-framework/issues/3872
() => shouldContain == dropdown.ChildrenOfType<Menu.DrawableMenuItem>().Any(i => i.ChildrenOfType<CompositeDrawable>().OfType<IHasText>().First().Text == collectionName));
private IconButton getAddOrRemoveButton(int index)
=> getCollectionDropdownItemAt(index).ChildrenOfType<IconButton>().Single();
private void addExpandHeaderStep() => AddStep("expand header", () =>
{
InputManager.MoveMouseTo(dropdown.ChildrenOfType<CollectionDropdown.ShearedDropdownHeader>().Single());
InputManager.Click(MouseButton.Left);
});
private void addClickAddOrRemoveButtonStep(int index) => AddStep("click add or remove button", () =>
{
InputManager.MoveMouseTo(getAddOrRemoveButton(index));
InputManager.Click(MouseButton.Left);
});
private Menu.DrawableMenuItem getCollectionDropdownItemAt(int index)
{
// todo: we should be able to use Items, but apparently that's not guaranteed to be ordered... see: https://github.com/ppy/osu-framework/pull/6079
CollectionFilterMenuItem item = dropdown.ChildrenOfType<CollectionDropdown>().Single().ItemSource.ElementAt(index);
return dropdown.ChildrenOfType<Menu.DrawableMenuItem>().Single(i => i.Item.Text.Value == item.CollectionName);
}
}
}
@@ -1,44 +0,0 @@
// 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.Localisation;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Screens.SelectV2.Wedge;
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneDifficultyNameContent : SongSelectComponentsTestScene
{
private DifficultyNameContent? difficultyNameContent;
[Test]
public void TestLocalBeatmap()
{
AddStep("set component", () => Child = difficultyNameContent = new LocalDifficultyNameContent());
AddAssert("difficulty name is not set", () => LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType<TruncatingSpriteText>().Single().Text));
AddAssert("author is not set", () => LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType<OsuHoverContainer>().Single().ChildrenOfType<OsuSpriteText>().Single().Text));
AddStep("set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(new Beatmap
{
BeatmapInfo = new BeatmapInfo
{
DifficultyName = "really long difficulty name that gets truncated",
Metadata = new BeatmapMetadata
{
Author = { Username = "really long username that is autosized" },
},
OnlineID = 1,
}
}));
AddAssert("difficulty name is set", () => !LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType<TruncatingSpriteText>().Single().Text));
AddAssert("author is set", () => !LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType<OsuHoverContainer>().Single().ChildrenOfType<OsuSpriteText>().Single().Text));
}
}
}
@@ -13,19 +13,19 @@ using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.SelectV2.Footer;
using osu.Game.Screens.SelectV2;
using osu.Game.Utils;
namespace osu.Game.Tests.Visual.UserInterface
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneScreenFooterButtonMods : OsuTestScene
public partial class TestSceneFooterButtonMods : OsuTestScene
{
private readonly TestScreenFooterButtonMods footerButtonMods;
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
public TestSceneScreenFooterButtonMods()
public TestSceneFooterButtonMods()
{
Add(footerButtonMods = new TestScreenFooterButtonMods(new TestModSelectOverlay())
{
@@ -98,9 +98,9 @@ namespace osu.Game.Tests.Visual.UserInterface
public void TestUnrankedBadge()
{
AddStep(@"Add unranked mod", () => changeMods(new[] { new OsuModDeflate() }));
AddUntilStep("Unranked badge shown", () => footerButtonMods.ChildrenOfType<ScreenFooterButtonMods.UnrankedBadge>().Single().Alpha == 1);
AddUntilStep("Unranked badge shown", () => footerButtonMods.ChildrenOfType<FooterButtonMods.UnrankedBadge>().Single().Alpha == 1);
AddStep(@"Clear selected mod", () => changeMods(Array.Empty<Mod>()));
AddUntilStep("Unranked badge not shown", () => footerButtonMods.ChildrenOfType<ScreenFooterButtonMods.UnrankedBadge>().Single().Alpha == 0);
AddUntilStep("Unranked badge not shown", () => footerButtonMods.ChildrenOfType<FooterButtonMods.UnrankedBadge>().Single().Alpha == 0);
}
private void changeMods(IReadOnlyList<Mod> mods) => footerButtonMods.Current.Value = mods;
@@ -122,7 +122,7 @@ namespace osu.Game.Tests.Visual.UserInterface
}
}
private partial class TestScreenFooterButtonMods : ScreenFooterButtonMods
private partial class TestScreenFooterButtonMods : FooterButtonMods
{
public new OsuSpriteText MultiplierText => base.MultiplierText;
@@ -20,7 +20,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.SelectV2.Leaderboards;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Resources;
using osu.Game.Users;
using osuTK;
@@ -53,14 +53,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0f, 2f),
Shear = new Vector2(OsuGame.SHEAR, 0)
Shear = OsuGame.SHEAR,
},
drawWidthText = new OsuSpriteText(),
};
foreach (var scoreInfo in getTestScores())
{
fillFlow.Add(new LeaderboardScoreV2(scoreInfo)
fillFlow.Add(new BeatmapLeaderboardScore(scoreInfo)
{
Rank = scoreInfo.Position,
IsPersonalBest = scoreInfo.User.Id == 2,
@@ -93,7 +93,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
foreach (var scoreInfo in getTestScores())
{
fillFlow.Add(new LeaderboardScoreV2(scoreInfo)
fillFlow.Add(new BeatmapLeaderboardScore(scoreInfo)
{
Rank = scoreInfo.Position,
IsPersonalBest = scoreInfo.User.Id == 2,
@@ -108,7 +108,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
[Test]
public void TestUseTheseModsDoesNotCopySystemMods()
{
LeaderboardScoreV2 score = null!;
BeatmapLeaderboardScore score = null!;
AddStep("create content", () =>
{
@@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0f, 2f),
Shear = new Vector2(OsuGame.SHEAR, 0)
Shear = OsuGame.SHEAR,
},
drawWidthText = new OsuSpriteText(),
};
@@ -146,7 +146,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
Date = DateTimeOffset.Now.AddYears(-2),
};
fillFlow.Add(score = new LeaderboardScoreV2(scoreInfo)
fillFlow.Add(score = new BeatmapLeaderboardScore(scoreInfo)
{
Rank = scoreInfo.Position,
Shear = Vector2.Zero,
@@ -1,16 +1,23 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Carousel;
using osu.Game.Online.Leaderboards;
using osu.Game.Overlays;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual.UserInterface;
@@ -18,14 +25,14 @@ using osuTK;
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneBeatmapCarouselV2DifficultyPanel : ThemeComparisonTestScene
public partial class TestScenePanelBeatmap : ThemeComparisonTestScene
{
[Resolved]
private BeatmapManager beatmaps { get; set; } = null!;
private BeatmapInfo beatmap = null!;
public TestSceneBeatmapCarouselV2DifficultyPanel()
public TestScenePanelBeatmap()
: base(false)
{
}
@@ -65,6 +72,19 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddToggleStep("mania ruleset", v => Ruleset.Value = v ? new ManiaRuleset().RulesetInfo : new OsuRuleset().RulesetInfo);
}
[Test]
public void TestLocalRank()
{
foreach (var rank in Enum.GetValues<ScoreRank>())
{
AddStep($"set {rank.GetDescription()} rank", () => this.ChildrenOfType<UpdateableRank>().ForEach(p =>
{
p.Show();
p.Rank = rank;
}));
}
}
protected override Drawable CreateContent()
{
return new FillFlowContainer
@@ -1,16 +1,23 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Carousel;
using osu.Game.Online.Leaderboards;
using osu.Game.Overlays;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual.UserInterface;
@@ -18,14 +25,14 @@ using osuTK;
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneBeatmapCarouselV2StandalonePanel : ThemeComparisonTestScene
public partial class TestScenePanelBeatmapStandalone : ThemeComparisonTestScene
{
[Resolved]
private BeatmapManager beatmaps { get; set; } = null!;
private BeatmapInfo beatmap = null!;
public TestSceneBeatmapCarouselV2StandalonePanel()
public TestScenePanelBeatmapStandalone()
: base(false)
{
}
@@ -65,6 +72,19 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddToggleStep("mania ruleset", v => Ruleset.Value = v ? new ManiaRuleset().RulesetInfo : new OsuRuleset().RulesetInfo);
}
[Test]
public void TestLocalRank()
{
foreach (var rank in Enum.GetValues<ScoreRank>())
{
AddStep($"set {rank.GetDescription()} rank", () => this.ChildrenOfType<UpdateableRank>().ForEach(p =>
{
p.Show();
p.Rank = rank;
}));
}
}
protected override Drawable CreateContent()
{
return new FillFlowContainer
@@ -0,0 +1,120 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Overlays;
using osu.Game.Graphics.Carousel;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Visual.UserInterface;
using osuTK;
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestScenePanelGroup : ThemeComparisonTestScene
{
public TestScenePanelGroup()
: base(false)
{
}
[Test]
public void TestGeneral()
{
AddStep("general", () => CreateThemedContent(OverlayColourScheme.Aquamarine));
}
[Test]
public void TestStars()
{
for (int i = 0; i <= 10; i++)
{
int star = i;
AddStep($"display {i} star(s)", () =>
{
ContentContainer.Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[]
{
(typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Aquamarine))
},
Child = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0f, 5f),
Children = new[]
{
new PanelGroupStarDifficulty
{
Item = new CarouselItem(new GroupDefinition(star, star.ToString()))
},
new PanelGroupStarDifficulty
{
Item = new CarouselItem(new GroupDefinition(star, star.ToString())),
KeyboardSelected = { Value = true },
},
new PanelGroupStarDifficulty
{
Item = new CarouselItem(new GroupDefinition(star, star.ToString())),
Expanded = { Value = true },
},
new PanelGroupStarDifficulty
{
Item = new CarouselItem(new GroupDefinition(star, star.ToString())),
Expanded = { Value = true },
KeyboardSelected = { Value = true },
},
},
}
};
});
}
}
protected override Drawable CreateContent()
{
return new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0f, 5f),
Children = new Drawable[]
{
new PanelGroup
{
Item = new CarouselItem(new GroupDefinition('A', "Group A"))
},
new PanelGroup
{
Item = new CarouselItem(new GroupDefinition('A', "Group A")),
KeyboardSelected = { Value = true }
},
new PanelGroup
{
Item = new CarouselItem(new GroupDefinition('A', "Group A")),
Expanded = { Value = true }
},
new PanelGroup
{
Item = new CarouselItem(new GroupDefinition('A', "Group A")),
KeyboardSelected = { Value = true },
Expanded = { Value = true }
},
}
};
}
}
}
@@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Carousel;
using osu.Game.Overlays;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Resources;
@@ -16,14 +17,14 @@ using osuTK;
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneBeatmapCarouselV2SetPanel : ThemeComparisonTestScene
public partial class TestScenePanelSet : ThemeComparisonTestScene
{
[Resolved]
private BeatmapManager beatmaps { get; set; } = null!;
private BeatmapSetInfo beatmapSet = null!;
public TestSceneBeatmapCarouselV2SetPanel()
public TestScenePanelSet()
: base(false)
{
}

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