1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-24 19:50:38 +08:00

Merge branch 'master' into freestyle-mods

This commit is contained in:
Bartłomiej Dach
2025-04-23 13:49:32 +02:00
committed by GitHub
Unverified
226 changed files with 8732 additions and 2517 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),
}
};
}
+1 -1
View File
@@ -8,6 +8,6 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModEasy : ModEasyWithExtraLives
{
public override LocalisableString Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and three lives!";
public override LocalisableString Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and extra lives!";
}
}
@@ -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 -1
View File
@@ -8,6 +8,6 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModEasy : ModEasyWithExtraLives
{
public override LocalisableString Description => @"More forgiving HP drain, less accuracy required, and three lives!";
public override LocalisableString Description => @"More forgiving HP drain, less accuracy required, and extra lives!";
}
}
@@ -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]);
}
}
}
+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),
}
};
}
+1 -1
View File
@@ -8,6 +8,6 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModEasy : ModEasyWithExtraLives
{
public override LocalisableString Description => @"Larger circles, more forgiving HP drain, less accuracy required, and three lives!";
public override LocalisableString Description => @"Larger circles, more forgiving HP drain, less accuracy required, and extra lives!";
}
}
+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))
@@ -176,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;
}
@@ -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;
@@ -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),
}
};
}
@@ -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;
}
}
}
@@ -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%"));
}
}
}
@@ -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()
{
@@ -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;
@@ -85,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",
@@ -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; }
}
}
}
@@ -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));
}
@@ -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
}
}
}
@@ -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()
{
@@ -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();
}
@@ -1056,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);
});
}
@@ -426,6 +426,31 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("countdown started", () => MultiplayerClient.ServerRoom!.ActiveCountdowns.Any());
}
[Test]
public void TestSettingsRemainsOpenOnRoomUpdate()
{
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("open settings", () => this.ChildrenOfType<MultiplayerMatchSettingsOverlay>().Single().Show());
AddAssert("settings opened", () => this.ChildrenOfType<MultiplayerMatchSettingsOverlay>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));
AddStep("trigger room update", () => MultiplayerClient.AddPlaylistItem(MultiplayerClient.ServerRoom!.Playlist[0].Clone()));
AddAssert("settings still open", () => this.ChildrenOfType<MultiplayerMatchSettingsOverlay>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));
}
private partial class TestMultiplayerMatchSubScreen : MultiplayerMatchSubScreen
{
[Resolved(canBeNull: true)]
@@ -6,8 +6,12 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Database;
@@ -16,10 +20,12 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual.OnlinePlay;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
@@ -153,10 +159,40 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
}
[Test]
public void TestFreeModSelectionDisable()
{
FooterButtonFreeMods freeMods = null!;
AddAssert("freestyle enabled", () => songSelect.Freestyle.Value, () => Is.True);
AddStep("click icon in free mods button", () =>
{
freeMods = this.ChildrenOfType<FooterButtonFreeMods>().Single();
InputManager.MoveMouseTo(freeMods.ChildrenOfType<SpriteIcon>().Single());
InputManager.Click(MouseButton.Left);
});
AddAssert("mod select not visible", () => this.ChildrenOfType<FreeModSelectOverlay>().Single().State.Value, () => Is.EqualTo(Visibility.Hidden));
AddStep("toggle freestyle off", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<FooterButtonFreestyle>().Single());
InputManager.Click(MouseButton.Left);
});
AddAssert("freestyle disabled", () => songSelect.Freestyle.Value, () => Is.False);
AddStep("click icon in free mods button", () =>
{
InputManager.MoveMouseTo(freeMods.ChildrenOfType<SpriteIcon>().Single());
InputManager.Click(MouseButton.Left);
});
AddAssert("mod select visible", () => this.ChildrenOfType<FreeModSelectOverlay>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));
}
private partial class TestPlaylistsSongSelect : PlaylistsSongSelect
{
public new MatchBeatmapDetailArea BeatmapDetails => (MatchBeatmapDetailArea)base.BeatmapDetails;
public new IBindable<bool> Freestyle => base.Freestyle;
public TestPlaylistsSongSelect(Room room)
: base(room)
{
@@ -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()
{
@@ -218,7 +218,7 @@ namespace osu.Game.Tests.Visual.Online
}
private void waitForLoad()
=> AddUntilStep("wait for panels to load", () => this.ChildrenOfType<LoadingSpinner>().Single().State.Value, () => Is.EqualTo(Visibility.Hidden));
=> AddUntilStep("wait for panels to load", () => this.ChildrenOfType<LoadingSpinner>().First().State.Value, () => Is.EqualTo(Visibility.Hidden));
private void assertVisiblePanelCount<T>(int expectedVisible)
where T : UserPanel
@@ -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>();
@@ -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()
@@ -0,0 +1,185 @@
// 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.Testing;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.SongSelectV2
{
[TestFixture]
public partial class TestSceneBeatmapCarouselUpdateHandling : BeatmapCarouselTestScene
{
private BeatmapSetInfo baseTestBeatmap = null!;
[SetUpSteps]
public void SetUpSteps()
{
RemoveAllBeatmaps();
CreateCarousel();
AddBeatmaps(1, 3);
AddStep("generate and add test beatmap", () =>
{
baseTestBeatmap = TestResources.CreateTestBeatmapSetInfo(3);
var metadata = new BeatmapMetadata
{
Artist = "update test",
Title = "beatmap",
};
foreach (var b in baseTestBeatmap.Beatmaps)
b.Metadata = metadata;
BeatmapSets.Add(baseTestBeatmap);
});
WaitForSorting();
}
[Test]
public void TestBeatmapSetUpdatedNoop()
{
List<Panel> originalDrawables = new List<Panel>();
AddStep("store drawable references", () =>
{
originalDrawables.Clear();
originalDrawables.AddRange(Carousel.ChildrenOfType<Panel>().ToList());
});
AddStep("update beatmap with same reference", () => BeatmapSets.ReplaceRange(1, 1, [baseTestBeatmap]));
WaitForSorting();
AddAssert("drawables unchanged", () => Carousel.ChildrenOfType<Panel>(), () => Is.EqualTo(originalDrawables));
}
[Test]
public void TestBeatmapSetMetadataUpdated()
{
var metadata = new BeatmapMetadata
{
Artist = "updated test",
Title = "new beatmap title",
};
List<Panel> originalDrawables = new List<Panel>();
AddStep("store drawable references", () =>
{
originalDrawables.Clear();
originalDrawables.AddRange(Carousel.ChildrenOfType<Panel>().ToList());
});
updateBeatmap(b => b.Metadata = metadata);
WaitForSorting();
AddAssert("drawables changed", () => Carousel.ChildrenOfType<Panel>(), () => Is.Not.EqualTo(originalDrawables));
}
[Test]
public void TestSelectionHeld()
{
SelectPrevGroup();
WaitForSelection(1, 0);
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
updateBeatmap();
WaitForSorting();
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
}
[Test] // Checks that we keep selection based on online ID where possible.
public void TestSelectionHeldDifficultyNameChanged()
{
SelectPrevGroup();
WaitForSelection(1, 0);
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
updateBeatmap(b => b.DifficultyName = "new name");
WaitForSorting();
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
}
[Test] // Checks that we fallback to keeping selection based on difficulty name.
public void TestSelectionHeldDifficultyOnlineIDChanged()
{
SelectPrevGroup();
WaitForSelection(1, 0);
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
updateBeatmap(b => b.OnlineID = b.OnlineID + 1);
WaitForSorting();
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
}
private void updateBeatmap(Action<BeatmapInfo>? updateBeatmap = null, Action<BeatmapSetInfo>? updateSet = null)
{
AddStep("update beatmap with different reference", () =>
{
var updatedSet = new BeatmapSetInfo
{
ID = baseTestBeatmap.ID,
OnlineID = baseTestBeatmap.OnlineID,
DateAdded = baseTestBeatmap.DateAdded,
DateSubmitted = baseTestBeatmap.DateSubmitted,
DateRanked = baseTestBeatmap.DateRanked,
Status = baseTestBeatmap.Status,
StatusInt = baseTestBeatmap.StatusInt,
DeletePending = baseTestBeatmap.DeletePending,
Hash = baseTestBeatmap.Hash,
Protected = baseTestBeatmap.Protected,
};
updateSet?.Invoke(updatedSet);
var updatedBeatmaps = baseTestBeatmap.Beatmaps.Select(b =>
{
var updatedBeatmap = new BeatmapInfo
{
ID = b.ID,
Metadata = b.Metadata,
Ruleset = b.Ruleset,
DifficultyName = b.DifficultyName,
BeatmapSet = updatedSet,
Status = b.Status,
OnlineID = b.OnlineID,
Length = b.Length,
BPM = b.BPM,
Hash = b.Hash,
StarRating = b.StarRating,
MD5Hash = b.MD5Hash,
OnlineMD5Hash = b.OnlineMD5Hash,
};
updateBeatmap?.Invoke(updatedBeatmap);
return updatedBeatmap;
}).ToList();
updatedSet.Beatmaps.AddRange(updatedBeatmaps);
int originalIndex = BeatmapSets.IndexOf(baseTestBeatmap);
BeatmapSets.ReplaceRange(originalIndex, 1, [updatedSet]);
});
}
}
}
@@ -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,181 @@
// 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.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Models;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Screens.SelectV2;
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneBeatmapMetadataWedge : SongSelectComponentsTestScene
{
private APIBeatmapSet? currentOnlineSet;
private BeatmapMetadataWedge wedge = null!;
protected override void LoadComplete()
{
base.LoadComplete();
((DummyAPIAccess)API).HandleRequest = request =>
{
switch (request)
{
case GetBeatmapSetRequest set:
if (set.ID == currentOnlineSet?.OnlineID)
{
set.TriggerSuccess(currentOnlineSet);
return true;
}
return false;
default:
return false;
}
};
Child = wedge = new BeatmapMetadataWedge
{
State = { Value = Visibility.Visible },
};
}
[Test]
public void TestShowHide()
{
AddStep("all metrics", () =>
{
var (working, onlineSet) = createTestBeatmap();
currentOnlineSet = onlineSet;
Beatmap.Value = working;
});
AddStep("hide wedge", () => wedge.Hide());
AddStep("show wedge", () => wedge.Show());
}
[Test]
public void TestVariousMetrics()
{
AddStep("all metrics", () =>
{
var (working, onlineSet) = createTestBeatmap();
currentOnlineSet = onlineSet;
Beatmap.Value = working;
});
AddStep("null beatmap", () => Beatmap.SetDefault());
AddStep("no source", () =>
{
var (working, onlineSet) = createTestBeatmap();
working.Metadata.Source = string.Empty;
currentOnlineSet = onlineSet;
Beatmap.Value = working;
});
AddStep("no success rate", () =>
{
var (working, onlineSet) = createTestBeatmap();
onlineSet.Beatmaps.Single().PlayCount = 0;
onlineSet.Beatmaps.Single().PassCount = 0;
currentOnlineSet = onlineSet;
Beatmap.Value = working;
});
AddStep("no user ratings", () =>
{
var (working, onlineSet) = createTestBeatmap();
onlineSet.Ratings = Array.Empty<int>();
currentOnlineSet = onlineSet;
Beatmap.Value = working;
});
AddStep("no fail times", () =>
{
var (working, onlineSet) = createTestBeatmap();
onlineSet.Beatmaps.Single().FailTimes = null;
currentOnlineSet = onlineSet;
Beatmap.Value = working;
});
AddStep("no metrics", () =>
{
var (working, onlineSet) = createTestBeatmap();
onlineSet.Ratings = Array.Empty<int>();
onlineSet.Beatmaps.Single().FailTimes = null;
currentOnlineSet = onlineSet;
Beatmap.Value = working;
});
AddStep("local beatmap", () =>
{
var (working, onlineSet) = createTestBeatmap();
working.BeatmapInfo.OnlineID = 0;
currentOnlineSet = onlineSet;
Beatmap.Value = working;
});
}
[Test]
public void TestTruncation()
{
AddStep("long text", () =>
{
var (working, onlineSet) = createTestBeatmap();
working.BeatmapInfo.Metadata.Author = new RealmUser { Username = "Verrrrryyyy llooonngggggg author" };
working.BeatmapInfo.Metadata.Source = "Verrrrryyyy llooonngggggg source";
working.BeatmapInfo.Metadata.Tags = string.Join(' ', Enumerable.Repeat(working.BeatmapInfo.Metadata.Tags, 3));
onlineSet.Genre = new BeatmapSetOnlineGenre { Id = 12, Name = "Verrrrryyyy llooonngggggg genre" };
onlineSet.Language = new BeatmapSetOnlineLanguage { Id = 12, Name = "Verrrrryyyy llooonngggggg language" };
currentOnlineSet = onlineSet;
Beatmap.Value = working;
});
}
private (WorkingBeatmap, APIBeatmapSet) createTestBeatmap()
{
var working = CreateWorkingBeatmap(Ruleset.Value);
var onlineSet = new APIBeatmapSet
{
OnlineID = working.BeatmapSetInfo.OnlineID,
Genre = new BeatmapSetOnlineGenre { Id = 15, Name = "Pop" },
Language = new BeatmapSetOnlineLanguage { Id = 15, Name = "English" },
Ratings = Enumerable.Range(0, 11).ToArray(),
Beatmaps = new[]
{
new APIBeatmap
{
OnlineID = working.BeatmapInfo.OnlineID,
PlayCount = 10000,
PassCount = 4567,
FailTimes = new APIFailTimes
{
Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
},
},
}
};
working.BeatmapSetInfo.DateSubmitted = DateTimeOffset.Now;
working.BeatmapSetInfo.DateRanked = DateTimeOffset.Now;
return (working, onlineSet);
}
}
}
@@ -0,0 +1,159 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Visual.SongSelect;
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneBeatmapTitleWedge : SongSelectComponentsTestScene
{
private RulesetStore rulesets = null!;
private BeatmapTitleWedge titleWedge = null!;
private BeatmapTitleWedge.DifficultyDisplay difficultyDisplay => titleWedge.ChildrenOfType<BeatmapTitleWedge.DifficultyDisplay>().Single();
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets)
{
this.rulesets = rulesets;
}
protected override void LoadComplete()
{
base.LoadComplete();
AddRange(new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
titleWedge = new BeatmapTitleWedge
{
State = { Value = Visibility.Visible },
},
},
}
});
AddSliderStep("change star difficulty", 0, 11.9, 4.18, v =>
{
difficultyDisplay.ChildrenOfType<StarRatingDisplay>().Single().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 testBeatmap = TestSceneBeatmapInfoWedge.CreateTestBeatmap(rulesetInfo);
setRuleset(rulesetInfo);
selectBeatmap(testBeatmap);
}
}
[Test]
public void TestNullBeatmap()
{
selectBeatmap(null);
AddAssert("check default title", () => titleWedge.DisplayedTitle == Beatmap.Default.BeatmapInfo.Metadata.Title);
AddAssert("check default artist", () => titleWedge.DisplayedArtist == Beatmap.Default.BeatmapInfo.Metadata.Artist);
AddAssert("check no statistics", () => difficultyDisplay.ChildrenOfType<BeatmapTitleWedge.DifficultyStatisticsDisplay>().All(d => !d.Statistics.Any()));
}
[Test]
public void TestBPMUpdates()
{
const double bpm = 120;
IBeatmap beatmap = TestSceneBeatmapInfoWedge.CreateTestBeatmap(new OsuRuleset().RulesetInfo);
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / bpm });
OsuModDoubleTime doubleTime = null!;
selectBeatmap(beatmap);
checkDisplayedBPM($"{bpm}");
AddStep("select DT", () => SelectedMods.Value = new[] { doubleTime = new OsuModDoubleTime() });
checkDisplayedBPM($"{bpm * 1.5f}");
AddStep("change DT rate", () => doubleTime.SpeedChange.Value = 2);
checkDisplayedBPM($"{bpm * 2}");
AddStep("select HT", () => SelectedMods.Value = new[] { new OsuModHalfTime() });
checkDisplayedBPM($"{bpm * 0.75f}");
}
[Test]
public void TestWedgeVisibility()
{
AddStep("hide", () => { titleWedge.Hide(); });
AddWaitStep("wait for hide", 3);
AddAssert("check visibility", () => titleWedge.Alpha == 0);
AddStep("show", () => { titleWedge.Show(); });
AddWaitStep("wait for show", 1);
AddAssert("check visibility", () => titleWedge.Alpha > 0);
}
[TestCase(120, 125, null, "120-125 (mostly 120)")]
[TestCase(120, 120.6, null, "120-121 (mostly 120)")]
[TestCase(120, 120.4, null, "120")]
[TestCase(120, 120.6, "DT", "180-182 (mostly 180)")]
[TestCase(120, 120.4, "DT", "180")]
public void TestVaryingBPM(double commonBpm, double otherBpm, string? mod, string expectedDisplay)
{
IBeatmap beatmap = TestSceneBeatmapInfoWedge.CreateTestBeatmap(new OsuRuleset().RulesetInfo);
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm });
beatmap.ControlPointInfo.Add(100, new TimingControlPoint { BeatLength = 60 * 1000 / otherBpm });
beatmap.ControlPointInfo.Add(200, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm });
if (mod != null)
AddStep($"select {mod}", () => SelectedMods.Value = new[] { Ruleset.Value.CreateInstance().CreateModFromAcronym(mod) });
selectBeatmap(beatmap);
checkDisplayedBPM(expectedDisplay);
}
private void setRuleset(RulesetInfo rulesetInfo)
{
AddStep("set ruleset", () => Ruleset.Value = rulesetInfo);
}
private void selectBeatmap(IBeatmap? b)
{
AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () =>
{
Beatmap.Value = b == null ? Beatmap.Default : CreateWorkingBeatmap(b);
});
}
private void checkDisplayedBPM(string target)
{
AddUntilStep($"displayed bpm is {target}", () =>
{
var label = titleWedge.ChildrenOfType<BeatmapTitleWedge.Statistic>().Single(l => l.TooltipText == BeatmapsetsStrings.ShowStatsBpm);
return label.Text == target;
});
}
}
}
@@ -0,0 +1,82 @@
// 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.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Graphics;
using osu.Game.Overlays;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Visual.UserInterface;
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneBeatmapTitleWedgeStatistic : ThemeComparisonTestScene
{
private BeatmapTitleWedge.StatisticPlayCount playCount = null!;
private BeatmapTitleWedge.Statistic statistic2 = null!;
private BeatmapTitleWedge.Statistic statistic3 = null!;
private BeatmapTitleWedge.Statistic statistic4 = null!;
public TestSceneBeatmapTitleWedgeStatistic()
: base(false)
{
}
[Test]
public void TestLoading()
{
AddStep("setup", () => CreateThemedContent(OverlayColourScheme.Aquamarine));
AddStep("set loading", () => this.ChildrenOfType<BeatmapTitleWedge.Statistic>().ForEach(s => s.Text = null));
AddWaitStep("wait", 3);
AddStep("set values", () =>
{
playCount.Value = new BeatmapTitleWedge.StatisticPlayCount.Data(1234, 12);
statistic2.Text = "3,234";
statistic3.Text = "12:34";
statistic4.Text = "123";
});
AddStep("set large values", () =>
{
playCount.Value = new BeatmapTitleWedge.StatisticPlayCount.Data(134587921, 502);
statistic2.Text = "1,048,576";
statistic3.Text = "2:50:23";
statistic4.Text = "1238014";
});
}
protected override Drawable CreateContent() => new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Direction = FillDirection.Horizontal,
AutoSizeAxes = Axes.Both,
Children = new[]
{
playCount = new BeatmapTitleWedge.StatisticPlayCount(true, minSize: 50)
{
Value = new BeatmapTitleWedge.StatisticPlayCount.Data(1234, 12),
},
statistic2 = new BeatmapTitleWedge.Statistic(OsuIcon.Clock, true, minSize: 30)
{
Text = "3,234",
TooltipText = "Statistic 2",
},
statistic3 = new BeatmapTitleWedge.Statistic(OsuIcon.Metronome)
{
Text = "12:34",
Margin = new MarginPadding { Right = 10f },
TooltipText = "Statistic 3",
},
statistic4 = new BeatmapTitleWedge.Statistic(OsuIcon.Graphics)
{
Text = "123",
TooltipText = "Statistic 4",
},
},
};
}
}
@@ -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));
}
}
}
@@ -0,0 +1,166 @@
// 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.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Game.Overlays;
using osu.Game.Screens.SelectV2;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneDifficultyStatisticsDisplay : OsuTestScene
{
private Container displayContainer = null!;
private BeatmapTitleWedge.DifficultyStatisticsDisplay display = null!;
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
[SetUpSteps]
public void SetUpSteps()
{
AddStep("setup", () =>
{
Child = displayContainer = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 300,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
},
display = new BeatmapTitleWedge.DifficultyStatisticsDisplay
{
RelativeSizeAxes = Axes.X,
Statistics = new[]
{
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 1", 0.5f, 0.5f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 2", 0.8f, 0.8f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 3", 0.7f, 0.7f, 1f),
}
}
}
};
});
AddSliderStep("display width", 0, 300, 300, v =>
{
if (displayContainer.IsNotNull())
displayContainer.Width = v;
});
}
[Test]
public void TestEmpty()
{
AddStep("set empty", () => display.Statistics = Array.Empty<BeatmapTitleWedge.StatisticDifficulty.Data>());
AddAssert("no statistics", () => !display.ChildrenOfType<BeatmapTitleWedge.StatisticDifficulty>().Any());
AddAssert("no tiny statistics", () => !display.ChildrenOfType<GridContainer>().Single().Content.Any());
}
[Test]
public void TestDisplay()
{
AddStep("change data with same labels", () => display.Statistics = new[]
{
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 1", 0.2f, 0.2f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 2", 0.7f, 0.7f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 3", 0.4f, 0.8f, 1f),
});
AddStep("change data with different labels", () => display.Statistics = new[]
{
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 4", 0.3f, 0.3f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 5", 0.8f, 0.8f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 6", 0.5f, 0.5f, 1f),
});
AddAssert("statistics visible", () => display.ChildrenOfType<BeatmapTitleWedge.StatisticDifficulty>().First().Parent!.Alpha == 1);
AddAssert("tiny statistics hidden", () => display.ChildrenOfType<GridContainer>().Last().Alpha == 0);
AddStep("shrink width", () => displayContainer.Width = 100);
AddAssert("statistics hidden", () => display.ChildrenOfType<BeatmapTitleWedge.StatisticDifficulty>().First().Parent!.Alpha == 0);
AddUntilStep("tiny statistics displayed", () => display.ChildrenOfType<GridContainer>().Last().Alpha == 1);
}
[Test]
public void TestContraction()
{
AddAssert("statistics visible", () => display.ChildrenOfType<BeatmapTitleWedge.StatisticDifficulty>().First().Parent!.Alpha == 1);
AddAssert("tiny statistics hidden", () => display.ChildrenOfType<GridContainer>().Last().Alpha == 0);
AddStep("set too many statistics", () => display.Statistics = new[]
{
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 1", 0.2f, 0.2f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 2", 0.7f, 0.7f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 3", 0.4f, 0.8f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 4", 0.3f, 0.3f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 5", 0.8f, 0.8f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 6", 0.5f, 0.5f, 1f),
});
AddAssert("statistics hidden", () => display.ChildrenOfType<BeatmapTitleWedge.StatisticDifficulty>().First().Parent!.Alpha == 0);
AddUntilStep("tiny statistics displayed", () => display.ChildrenOfType<GridContainer>().Last().Alpha == 1);
AddStep("set less statistics", () => display.Statistics = new[]
{
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 1", 0.2f, 0.2f, 1f),
});
AddAssert("tiny statistics hidden", () => display.ChildrenOfType<GridContainer>().Last().Alpha == 0);
AddUntilStep("statistics visible", () => display.ChildrenOfType<BeatmapTitleWedge.StatisticDifficulty>().First().Parent!.Alpha == 1);
}
[Test]
public void TestAutoSize()
{
AddStep("setup auto size", () => Child = display = new BeatmapTitleWedge.DifficultyStatisticsDisplay(true)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Statistics = new[]
{
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 1", 0.5f, 0.5f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 2", 0.8f, 0.8f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 3", 0.7f, 0.7f, 1f),
}
});
AddAssert("statistics visible", () => display.ChildrenOfType<BeatmapTitleWedge.StatisticDifficulty>().First().Parent!.Alpha == 1);
AddAssert("tiny statistics hidden", () => display.ChildrenOfType<GridContainer>().Last().Alpha == 0);
AddStep("set too many statistics", () => display.Statistics = new[]
{
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 1", 0.2f, 0.2f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 2", 0.7f, 0.7f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 3", 0.4f, 0.8f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 4", 0.3f, 0.3f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 5", 0.8f, 0.8f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 6", 0.5f, 0.5f, 1f),
});
AddAssert("statistics still visible", () => display.ChildrenOfType<BeatmapTitleWedge.StatisticDifficulty>().First().Parent!.Alpha == 1);
AddAssert("tiny statistics still hidden", () => display.ChildrenOfType<GridContainer>().Last().Alpha == 0);
AddStep("set less statistics", () => display.Statistics = new[]
{
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 1", 0.2f, 0.2f, 1f),
});
AddAssert("statistics still visible", () => display.ChildrenOfType<BeatmapTitleWedge.StatisticDifficulty>().First().Parent!.Alpha == 1);
AddAssert("tiny statistics still hidden", () => display.ChildrenOfType<GridContainer>().Last().Alpha == 0);
}
}
}
@@ -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)
{
}
@@ -9,14 +9,14 @@ using osu.Game.Screens.SelectV2;
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneUpdateBeatmapSetButtonV2 : OsuTestScene
public partial class TestScenePanelUpdateBeatmapButton : OsuTestScene
{
private UpdateBeatmapSetButton button = null!;
private PanelUpdateBeatmapButton button = null!;
[SetUp]
public void SetUp() => Schedule(() =>
{
Child = button = new UpdateBeatmapSetButton
Child = button = new PanelUpdateBeatmapButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -15,9 +15,9 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Screens.Footer;
using osu.Game.Screens.SelectV2.Footer;
using osu.Game.Screens.SelectV2;
namespace osu.Game.Tests.Visual.UserInterface
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneScreenFooter : OsuManualInputManagerTestScene
{
@@ -51,9 +51,9 @@ namespace osu.Game.Tests.Visual.UserInterface
screenFooter.SetButtons(new ScreenFooterButton[]
{
new ScreenFooterButtonMods(modOverlay) { Current = SelectedMods },
new ScreenFooterButtonRandom(),
new ScreenFooterButtonOptions(),
new FooterButtonMods(modOverlay) { Current = SelectedMods },
new FooterButtonRandom(),
new FooterButtonOptions(),
});
});
@@ -22,7 +22,7 @@ using osu.Game.Rulesets.Taiko;
using osu.Game.Screens;
using osu.Game.Screens.Footer;
using osu.Game.Screens.Menu;
using osu.Game.Screens.SelectV2.Footer;
using osu.Game.Screens.SelectV2;
using osuTK.Input;
namespace osu.Game.Tests.Visual.SongSelectV2
@@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
{
base.SetUpSteps();
AddStep("load screen", () => Stack.Push(new Screens.SelectV2.SoloSongSelect()));
AddStep("load screen", () => Stack.Push(new SoloSongSelect()));
AddUntilStep("wait for load", () => Stack.CurrentScreen is Screens.SelectV2.SongSelect songSelect && songSelect.IsLoaded);
}
@@ -199,7 +199,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
{
AddStep("Press F1", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<ScreenFooterButtonMods>().Single());
InputManager.MoveMouseTo(this.ChildrenOfType<FooterButtonMods>().Single());
InputManager.Click(MouseButton.Left);
});
AddAssert("Overlay visible", () => this.ChildrenOfType<ModSelectOverlay>().Single().State.Value == Visibility.Visible);
@@ -2,10 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
@@ -14,6 +16,9 @@ namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneFPSCounter : OsuTestScene
{
[Resolved]
private OsuConfigManager config { get; set; } = null!;
[SetUpSteps]
public void SetUpSteps()
{
@@ -41,6 +46,7 @@ namespace osu.Game.Tests.Visual.UserInterface
},
};
});
AddToggleStep("toggle show", b => config.SetValue(OsuSetting.ShowFpsDisplay, b));
}
[Test]
@@ -0,0 +1,132 @@
// 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.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneShearAligningWrapper : OsuTestScene
{
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
private ShearedBox first = null!;
private ShearedBox second = null!;
private ShearedBox third = null!;
[SetUp]
public void SetUp() => Schedule(() =>
{
Child = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 200f,
AutoSizeAxes = Axes.Y,
Shear = OsuGame.SHEAR,
CornerRadius = 10f,
Masking = true,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background6,
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0f, 10f),
Children = new Drawable[]
{
new ShearAligningWrapper(first = new ShearedBox("Text 1", OsuColour.Gray(0.4f))
{
RelativeSizeAxes = Axes.X,
Height = 30,
}),
new ShearAligningWrapper(second = new ShearedBox("Text 2", OsuColour.Gray(0.3f))
{
RelativeSizeAxes = Axes.X,
Height = 30,
}),
new ShearAligningWrapper(third = new ShearedBox("Text 3", OsuColour.Gray(0.2f))
{
RelativeSizeAxes = Axes.X,
Height = 30,
}),
}
}
},
};
});
[SetUpSteps]
public void SetUpSteps()
{
AddSliderStep("box 1 height", 0, 100, 30, h =>
{
if (first.IsNotNull())
first.Height = h;
});
AddSliderStep("box 2 height", 0, 100, 30, h =>
{
if (second.IsNotNull())
second.Height = h;
});
AddSliderStep("box 3 height", 0, 100, 30, h =>
{
if (third.IsNotNull())
third.Height = h;
});
}
public partial class ShearedBox : Container
{
private readonly string text;
private readonly Color4 boxColour;
public ShearedBox(string text, Color4 boxColour)
{
this.text = text;
this.boxColour = boxColour;
}
[BackgroundDependencyLoader]
private void load()
{
CornerRadius = 10;
Masking = true;
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = boxColour,
},
new OsuSpriteText
{
Text = text,
Colour = Color4.White,
Shear = -OsuGame.SHEAR,
Font = OsuFont.Torus.With(size: 24),
Margin = new MarginPadding { Left = 50 },
}
};
}
}
}
}
@@ -53,6 +53,14 @@ namespace osu.Game.Tournament
return new ProductionEndpointConfiguration();
}
public override void SetHost(GameHost host)
{
base.SetHost(host);
if (host.Window != null)
host.Window.Title = $"{Name} [tournament client]";
}
private TournamentSpriteText initialisationText = null!;
[BackgroundDependencyLoader]
+1 -1
View File
@@ -14,10 +14,10 @@ namespace osu.Game.Beatmaps
/// This is a special status given when local changes are made via the editor.
/// Once in this state, online status changes should be ignored unless the beatmap is reverted or submitted.
/// </summary>
[Description("Local")]
[LocalisableDescription(typeof(SongSelectStrings), nameof(SongSelectStrings.LocallyModified))]
LocallyModified = -4,
[Description("Unknown")]
None = -3,
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowStatusGraveyard))]
+13 -1
View File
@@ -16,7 +16,19 @@ namespace osu.Game.Beatmaps
/// </summary>
public Func<Drawable> CreateIcon;
public string Content;
/// <summary>
/// The name of this statistic.
/// </summary>
public LocalisableString Name;
/// <summary>
/// The text representing the value of this statistic.
/// </summary>
public string Content;
/// <summary>
/// The length of a bar which visually represents this statistic's relevance in the beatmap.
/// </summary>
public float? BarDisplayLength;
}
}
@@ -19,9 +19,10 @@ namespace osu.Game.Beatmaps.Drawables
{
public partial class BeatmapSetOnlineStatusPill : CircularContainer, IHasTooltip
{
private const double animation_duration = 400;
private BeatmapOnlineStatus status;
/// <summary>
/// Whether to show <see cref="BeatmapOnlineStatus.None"/> as "unknown" instead of fading out.
/// </summary>
public bool ShowUnknownStatus { get; init; }
public BeatmapOnlineStatus Status
{
@@ -34,30 +35,27 @@ namespace osu.Game.Beatmaps.Drawables
status = value;
if (IsLoaded)
{
AutoSizeDuration = (float)animation_duration;
AutoSizeEasing = Easing.OutQuint;
updateState();
}
}
}
private BeatmapOnlineStatus status;
public float TextSize
{
get => statusText.Font.Size;
set => statusText.Font = statusText.Font.With(size: value);
init => statusText.Font = statusText.Font.With(size: value);
}
public MarginPadding TextPadding
{
get => statusText.Padding;
set => statusText.Padding = value;
init => statusText.Padding = value;
}
private readonly OsuSpriteText statusText;
private readonly Box background;
private const double animation_duration = 400;
[Resolved]
private OsuColour colours { get; set; } = null!;
@@ -66,6 +64,7 @@ namespace osu.Game.Beatmaps.Drawables
public BeatmapSetOnlineStatusPill()
{
AutoSizeAxes = Axes.Both;
Masking = true;
Alpha = 0;
@@ -99,14 +98,27 @@ namespace osu.Game.Beatmaps.Drawables
private void updateState()
{
if (Status == BeatmapOnlineStatus.None)
if (Status == BeatmapOnlineStatus.None && !ShowUnknownStatus)
{
Hide();
this.FadeOut(animation_duration, Easing.OutQuint);
return;
}
// The autosize animation on this component is intended to animate horizontal sizing only.
// To avoid vertical autosize animating from zero to non-zero, only apply the duration
// after we have a valid size.
if (Height > 0)
{
AutoSizeDuration = (float)animation_duration;
AutoSizeEasing = Easing.OutQuint;
}
this.FadeIn(animation_duration, Easing.OutQuint);
// Handle the case where transition from hidden to non-hidden may cause
// a fade from a colour that doesn't make sense (due to not being able to see the previous colour).
double duration = Alpha > 0 ? animation_duration : 0;
Color4 statusTextColour;
if (colourProvider != null)
@@ -114,8 +126,8 @@ namespace osu.Game.Beatmaps.Drawables
else
statusTextColour = status == BeatmapOnlineStatus.Graveyard ? colours.GreySeaFoamLight : Color4.Black;
statusText.FadeColour(statusTextColour, animation_duration, Easing.OutQuint);
background.FadeColour(OsuColour.ForBeatmapSetOnlineStatus(Status) ?? colourProvider?.Light1 ?? colours.GreySeaFoamLighter, animation_duration, Easing.OutQuint);
statusText.FadeColour(statusTextColour, duration, Easing.OutQuint);
background.FadeColour(OsuColour.ForBeatmapSetOnlineStatus(Status) ?? colourProvider?.Light1 ?? colours.GreySeaFoamLighter, duration, Easing.OutQuint);
statusText.Text = Status.GetLocalisableDescription().ToUpper();
}
@@ -30,7 +30,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{
new BeatmapSetOnlineStatusPill
{
AutoSizeAxes = Axes.Both,
Status = beatmapSet.Status,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
@@ -141,7 +141,7 @@ namespace osu.Game.Beatmaps.Drawables
Add(countText = new OsuSpriteText
{
Font = OsuFont.Default.With(size: 12),
Font = OsuFont.Style.Caption1,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Padding = new MarginPadding { Bottom = 1 }
@@ -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.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
@@ -24,6 +23,8 @@ namespace osu.Game.Beatmaps.Drawables
/// </summary>
public partial class StarRatingDisplay : CompositeDrawable, IHasCurrentValue<StarDifficulty>
{
public const double TRANSFORM_DURATION = 750;
private readonly bool animated;
private readonly Box background;
private readonly SpriteIcon starIcon;
@@ -37,9 +38,13 @@ namespace osu.Game.Beatmaps.Drawables
set => current.Current = value;
}
private readonly Bindable<double> displayedStars = new BindableDouble();
/// <summary>
/// The difficulty colour currently displayed.
/// Can be used to have other components match the spectrum animation.
/// </summary>
public Color4 DisplayedDifficultyColour => background.Colour;
private readonly Container textContainer;
private readonly Bindable<double> displayedStars = new BindableDouble();
/// <summary>
/// The currently displayed stars of this display wrapped in a bindable.
@@ -119,19 +124,14 @@ namespace osu.Game.Beatmaps.Drawables
Size = new Vector2(8f),
},
Empty(),
textContainer = new Container
starsText = new OsuSpriteText
{
AutoSizeAxes = Axes.Y,
Child = starsText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Margin = new MarginPadding { Bottom = 1.5f },
// todo: this should be size: 12f, but to match up with the design, it needs to be 14.4f
// see https://github.com/ppy/osu-framework/issues/3271.
Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold),
Shadow = false,
},
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Margin = new MarginPadding { Bottom = 1.5f },
Spacing = new Vector2(-1.4f),
Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold, fixedWidth: true),
Shadow = false,
},
}
}
@@ -147,7 +147,7 @@ namespace osu.Game.Beatmaps.Drawables
Current.BindValueChanged(c =>
{
if (animated)
this.TransformBindableTo(displayedStars, c.NewValue.Stars, 750, Easing.OutQuint);
this.TransformBindableTo(displayedStars, c.NewValue.Stars, TRANSFORM_DURATION, Easing.OutQuint);
else
displayedStars.Value = c.NewValue.Stars;
});
@@ -160,13 +160,8 @@ namespace osu.Game.Beatmaps.Drawables
background.Colour = colours.ForStarDifficulty(s.NewValue);
starIcon.Colour = s.NewValue >= 6.5 ? colours.Orange1 : colourProvider?.Background5 ?? Color4Extensions.FromHex("303d47");
starsText.Colour = s.NewValue >= 6.5 ? colours.Orange1 : colourProvider?.Background5 ?? Color4.Black.Opacity(0.75f);
// In order to avoid autosize throwing the width of these displays all over the place,
// let's lock in some sane defaults for the text width based on how many digits we're
// displaying.
textContainer.Width = 24 + Math.Max(starsText.Text.ToString().Length - 4, 0) * 6;
starIcon.Colour = s.NewValue >= OsuColour.STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF ? colours.Orange1 : colourProvider?.Background5 ?? Color4Extensions.FromHex("303d47");
starsText.Colour = s.NewValue >= OsuColour.STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF ? colours.Orange1 : colourProvider?.Background5 ?? Color4.Black.Opacity(0.75f);
}, true);
}
}
+1 -7
View File
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using osu.Framework;
using osu.Framework.Bindables;
using osu.Framework.Configuration;
@@ -263,10 +262,6 @@ namespace osu.Game.Configuration
public override TrackedSettings CreateTrackedSettings()
{
// these need to be assigned in normal game startup scenarios.
Debug.Assert(LookupKeyBindings != null);
Debug.Assert(LookupSkinName != null);
return new TrackedSettings
{
new TrackedSetting<bool>(OsuSetting.ShowFpsDisplay, state => new SettingDescription(
@@ -330,8 +325,7 @@ namespace osu.Game.Configuration
}
public Func<Guid, string> LookupSkinName { private get; set; } = _ => @"unknown";
public Func<GlobalAction, LocalisableString> LookupKeyBindings { get; set; } = _ => @"unknown";
public Func<GlobalAction, LocalisableString> LookupKeyBindings { private get; set; } = _ => @"unknown";
IBindable<float> IGameplaySettings.ComboColourNormalisationAmount => GetOriginalBindable<float>(OsuSetting.ComboColourNormalisationAmount);
IBindable<float> IGameplaySettings.PositionalHitsoundsLevel => GetOriginalBindable<float>(OsuSetting.PositionalHitsoundsLevel);
+25 -15
View File
@@ -30,7 +30,8 @@ namespace osu.Game.Database
public override IBindableList<BeatmapSetInfo> GetBeatmapSets(CancellationToken? cancellationToken)
{
loaded.Wait(cancellationToken ?? CancellationToken.None);
return detachedBeatmapSets.GetBoundCopy();
lock (detachedBeatmapSets)
return detachedBeatmapSets.GetBoundCopy();
}
[BackgroundDependencyLoader]
@@ -65,8 +66,11 @@ namespace osu.Game.Database
{
var detached = frozenSets.Detach();
detachedBeatmapSets.Clear();
detachedBeatmapSets.AddRange(detached);
lock (detachedBeatmapSets)
{
detachedBeatmapSets.Clear();
detachedBeatmapSets.AddRange(detached);
}
});
}
finally
@@ -116,22 +120,28 @@ namespace osu.Game.Database
if (!loaded.IsSet)
return;
// If this ever leads to performance issues, we could dequeue a limited number of operations per update frame.
while (pendingOperations.TryDequeue(out var op))
if (pendingOperations.Count == 0)
return;
lock (detachedBeatmapSets)
{
switch (op.Type)
// If this ever leads to performance issues, we could dequeue a limited number of operations per update frame.
while (pendingOperations.TryDequeue(out var op))
{
case OperationType.Insert:
detachedBeatmapSets.Insert(op.Index, op.BeatmapSet!);
break;
switch (op.Type)
{
case OperationType.Insert:
detachedBeatmapSets.Insert(op.Index, op.BeatmapSet!);
break;
case OperationType.Update:
detachedBeatmapSets.ReplaceRange(op.Index, 1, new[] { op.BeatmapSet! });
break;
case OperationType.Update:
detachedBeatmapSets.ReplaceRange(op.Index, 1, new[] { op.BeatmapSet! });
break;
case OperationType.Remove:
detachedBeatmapSets.RemoveAt(op.Index);
break;
case OperationType.Remove:
detachedBeatmapSets.RemoveAt(op.Index);
break;
}
}
}
}
@@ -0,0 +1,51 @@
// 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.Globalization;
using System.Numerics;
using osu.Game.Utils;
namespace osu.Game.Extensions
{
public static class NumberFormattingExtensions
{
/// <summary>
/// For a given numeric type, return a formatted string in the standard format we use for display everywhere.
/// </summary>
/// <param name="value">The numeric value.</param>
/// <param name="maxDecimalDigits">The maximum number of decimals to be considered in the original value.</param>
/// <param name="asPercentage">Whether the output should be a percentage. For integer types, 0-100 is mapped to 0-100%; for other types 0-1 is mapped to 0-100%.</param>
/// <returns>The formatted output.</returns>
public static string ToStandardFormattedString<T>(this T value, int maxDecimalDigits, bool asPercentage) where T : struct, INumber<T>, IMinMaxValue<T>
{
double floatValue = double.CreateTruncating(value);
decimal decimalPrecision = normalise(decimal.CreateTruncating(value), maxDecimalDigits);
// Find the number of significant digits (we could have less than maxDecimalDigits after normalize())
int significantDigits = FormatUtils.FindPrecision(decimalPrecision);
if (asPercentage)
{
if (value is int)
floatValue /= 100;
return floatValue.ToString($@"0.{new string('0', Math.Max(0, significantDigits - 2))}%", CultureInfo.InvariantCulture);
}
string negativeSign = Math.Round(floatValue, significantDigits) < 0 ? "-" : string.Empty;
return FormattableString.Invariant($"{negativeSign}{Math.Abs(floatValue).ToString($"N{significantDigits}")}");
}
/// <summary>
/// Removes all non-significant digits, keeping at most a requested number of decimal digits.
/// </summary>
/// <param name="d">The decimal to normalize.</param>
/// <param name="sd">The maximum number of decimal digits to keep. The final result may have fewer decimal digits than this value.</param>
/// <returns>The normalised decimal.</returns>
private static decimal normalise(decimal d, int sd)
=> decimal.Parse(Math.Round(d, sd).ToString(string.Concat("0.", new string('#', sd)), CultureInfo.InvariantCulture), CultureInfo.InvariantCulture);
}
}
@@ -24,7 +24,7 @@ using osu.Game.Input.Bindings;
using osuTK;
using osuTK.Input;
namespace osu.Game.Screens.SelectV2
namespace osu.Game.Graphics.Carousel
{
/// <summary>
/// A highly efficient vertical list display that is used primarily for the song select screen,
@@ -38,12 +38,12 @@ namespace osu.Game.Screens.SelectV2
/// <summary>
/// Height of the area above the carousel that should be treated as visible due to transparency of elements in front of it.
/// </summary>
public float BleedTop { get; set; } = 0;
public float BleedTop { get; set; }
/// <summary>
/// Height of the area below the carousel that should be treated as visible due to transparency of elements in front of it.
/// </summary>
public float BleedBottom { get; set; } = 0;
public float BleedBottom { get; set; }
/// <summary>
/// The number of pixels outside the carousel's vertical bounds to manifest drawables.
@@ -166,6 +166,11 @@ namespace osu.Game.Screens.SelectV2
/// </summary>
protected virtual Task FilterAsync() => filterTask = performFilter();
/// <summary>
/// Check whether two models are the same for display purposes.
/// </summary>
protected virtual bool CheckModelEquality(object x, object y) => ReferenceEquals(x, y);
/// <summary>
/// Create a drawable for the given carousel item so it can be displayed.
/// </summary>
@@ -228,6 +233,7 @@ namespace osu.Game.Screens.SelectV2
{
InternalChild = Scroll = new CarouselScrollContainer
{
Masking = false,
RelativeSizeAxes = Axes.Both,
};
@@ -489,11 +495,11 @@ namespace osu.Game.Screens.SelectV2
updateItemYPosition(item, ref lastVisible, ref yPos);
if (ReferenceEquals(item.Model, currentKeyboardSelection.Model))
currentKeyboardSelection = new Selection(item.Model, item, item.CarouselYPosition, i);
if (CheckModelEquality(item.Model, currentKeyboardSelection.Model!))
currentKeyboardSelection = new Selection(currentKeyboardSelection.Model, item, item.CarouselYPosition, i);
if (ReferenceEquals(item.Model, currentSelection.Model))
currentSelection = new Selection(item.Model, item, item.CarouselYPosition, i);
if (CheckModelEquality(item.Model, currentSelection.Model!))
currentSelection = new Selection(currentSelection.Model, item, item.CarouselYPosition, i);
}
// If a keyboard selection is currently made, we want to keep the view stable around the selection.
@@ -505,7 +511,7 @@ namespace osu.Game.Screens.SelectV2
private void scrollToSelection()
{
if (currentKeyboardSelection.CarouselItem != null)
Scroll.ScrollTo(currentKeyboardSelection.CarouselItem.CarouselYPosition - visibleHalfHeight);
Scroll.ScrollTo(currentKeyboardSelection.CarouselItem.CarouselYPosition - visibleHalfHeight + BleedTop);
}
#endregion
@@ -519,17 +525,17 @@ namespace osu.Game.Screens.SelectV2
/// <summary>
/// The position of the lower visible bound with respect to the current scroll position.
/// </summary>
private float visibleBottomBound => (float)(Scroll.Current + DrawHeight + BleedBottom);
private float visibleBottomBound;
/// <summary>
/// The position of the upper visible bound with respect to the current scroll position.
/// </summary>
private float visibleUpperBound => (float)(Scroll.Current - BleedTop);
private float visibleUpperBound;
/// <summary>
/// Half the height of the visible content.
/// </summary>
private float visibleHalfHeight => (DrawHeight + BleedBottom + BleedTop) / 2;
private float visibleHalfHeight;
protected override void Update()
{
@@ -538,6 +544,10 @@ namespace osu.Game.Screens.SelectV2
if (carouselItems == null)
return;
visibleBottomBound = (float)(Scroll.Current + DrawHeight + BleedBottom);
visibleUpperBound = (float)(Scroll.Current - BleedTop);
visibleHalfHeight = (DrawHeight + BleedBottom + BleedTop) / 2;
if (!selectionValid.IsValid)
{
refreshAfterSelection();
@@ -573,7 +583,7 @@ namespace osu.Game.Screens.SelectV2
panel.X = GetPanelXOffset(panel);
c.Selected.Value = c.Item == currentSelection?.CarouselItem;
c.Selected.Value = currentSelection?.CarouselItem != null && CheckModelEquality(c.Item, currentSelection.CarouselItem);
c.KeyboardSelected.Value = c.Item == currentKeyboardSelection?.CarouselItem;
c.Expanded.Value = c.Item.IsExpanded;
}
@@ -582,7 +592,7 @@ namespace osu.Game.Screens.SelectV2
protected virtual float GetPanelXOffset(Drawable panel)
{
Vector2 posInScroll = Scroll.ToLocalSpace(panel.ScreenSpaceDrawQuad.Centre);
float dist = Math.Abs(1f - posInScroll.Y / visibleHalfHeight);
float dist = Math.Abs(1f - (posInScroll.Y + BleedTop) / visibleHalfHeight);
return offsetX(dist, visibleHalfHeight);
}
@@ -639,7 +649,10 @@ namespace osu.Game.Screens.SelectV2
// The case where we're intending to display this panel, but it's already displayed.
// Note that we **must compare the model here** as the CarouselItems may be fresh instances due to a filter operation.
var existing = toDisplay.FirstOrDefault(i => i.Model == carouselPanel.Item!.Model);
//
// Reference equality is used here instead of CheckModelEquality intentionally. In order to switch to `CheckModelEquality`,
// we need a way to signal to the drawable panels that there is an update.
var existing = toDisplay.FirstOrDefault(i => ReferenceEquals(i.Model, carouselPanel.Item!.Model));
if (existing != null)
{
@@ -3,7 +3,7 @@
using System;
namespace osu.Game.Screens.SelectV2
namespace osu.Game.Graphics.Carousel
{
/// <summary>
/// Represents a single display item for display in a <see cref="Carousel{T}"/>.
@@ -11,7 +11,7 @@ namespace osu.Game.Screens.SelectV2
/// </summary>
public sealed class CarouselItem : IComparable<CarouselItem>
{
public const float DEFAULT_HEIGHT = 50;
public const float DEFAULT_HEIGHT = 45;
/// <summary>
/// The model this item is representing.
@@ -44,6 +44,11 @@ namespace osu.Game.Screens.SelectV2
/// </summary>
public bool IsExpanded { get; set; }
/// <summary>
/// The number of nested items underneath this header. Should only be used for headers of groups.
/// </summary>
public int NestedItemCount { get; set; }
public CarouselItem(object model)
{
Model = model;
@@ -5,7 +5,7 @@ using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace osu.Game.Screens.SelectV2
namespace osu.Game.Graphics.Carousel
{
/// <summary>
/// An interface representing a filter operation which can be run on a <see cref="Carousel{T}"/>.
@@ -5,7 +5,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Pooling;
namespace osu.Game.Screens.SelectV2
namespace osu.Game.Graphics.Carousel
{
/// <summary>
/// An interface to be attached to any <see cref="Drawable"/>s which are used for display inside a <see cref="Carousel{T}"/>.
@@ -134,7 +134,7 @@ namespace osu.Game.Graphics.Containers
protected virtual DrawableLinkCompiler CreateLinkCompiler(ITextPart textPart) => new DrawableLinkCompiler(textPart);
protected override FillFlowContainer CreateFlow() => new LinkFlow();
protected override InnerFlow CreateFlow() => new LinkFlow();
private partial class LinkFlow : InnerFlow
{
@@ -0,0 +1,49 @@
// 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.Framework.Layout;
using osuTK;
namespace osu.Game.Graphics.Containers
{
/// <summary>
/// Adds left padding based on direct parent to make sheared pieces in a vertical flow aligned appropriately.
/// </summary>
/// <remarks>
/// See associated test scene for further demonstration.
/// </remarks>
public partial class ShearAligningWrapper : CompositeDrawable
{
private readonly LayoutValue layout = new LayoutValue(Invalidation.MiscGeometry);
public ShearAligningWrapper(Drawable drawable)
{
RelativeSizeAxes = drawable.RelativeSizeAxes;
AutoSizeAxes = Axes.Both & ~drawable.RelativeSizeAxes;
InternalChild = drawable;
AddLayout(layout);
}
protected override void Update()
{
base.Update();
if (!layout.IsValid)
{
updateLayout();
layout.Validate();
}
}
private void updateLayout()
{
float shearWidth = OsuGame.SHEAR.X * Parent!.DrawHeight;
float relativeY = Parent!.DrawHeight == 0 ? 0 : InternalChild.ToSpaceOfOtherDrawable(Vector2.Zero, Parent).Y / Parent!.DrawHeight;
Padding = new MarginPadding { Left = shearWidth * relativeY };
}
}
}
+20 -3
View File
@@ -21,9 +21,11 @@ namespace osu.Game.Graphics
public static Color4 Gray(byte amt) => new Color4(amt, amt, amt, 255);
/// <summary>
/// Retrieves the colour for a given point in the star range.
/// The maximum star rating colour which can be distinguished against a black background.
/// </summary>
public Color4 ForStarDifficulty(double starDifficulty) => ColourUtils.SampleFromLinearGradient(new[]
public const float STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF = 6.5f;
public static readonly (float, Color4)[] STAR_DIFFICULTY_SPECTRUM =
{
(0.1f, Color4Extensions.FromHex("aaaaaa")),
(0.1f, Color4Extensions.FromHex("4290fb")),
@@ -37,7 +39,13 @@ namespace osu.Game.Graphics
(6.7f, Color4Extensions.FromHex("6563de")),
(7.7f, Color4Extensions.FromHex("18158e")),
(9.0f, Color4.Black),
}, (float)Math.Round(starDifficulty, 2, MidpointRounding.AwayFromZero));
(10.0f, Color4.Black),
};
/// <summary>
/// Retrieves the colour for a given point in the star range.
/// </summary>
public Color4 ForStarDifficulty(double starDifficulty) => ColourUtils.SampleFromLinearGradient(STAR_DIFFICULTY_SPECTRUM, (float)Math.Round(starDifficulty, 2, MidpointRounding.AwayFromZero));
/// <summary>
/// Retrieves the colour for a <see cref="ScoreRank"/>.
@@ -120,6 +128,9 @@ namespace osu.Game.Graphics
{
switch (status)
{
case BeatmapOnlineStatus.None:
return Color4.RosyBrown;
case BeatmapOnlineStatus.LocallyModified:
return Color4.OrangeRed;
@@ -403,6 +414,12 @@ namespace osu.Game.Graphics
public readonly Color4 Orange3 = Color4Extensions.FromHex(@"cca633");
public readonly Color4 Orange4 = Color4Extensions.FromHex(@"6b5c2e");
public readonly Color4 DarkOrange0 = Color4Extensions.FromHex(@"ffbb99");
public readonly Color4 DarkOrange1 = Color4Extensions.FromHex(@"ff9966");
public readonly Color4 DarkOrange2 = Color4Extensions.FromHex(@"eb7e47");
public readonly Color4 DarkOrange3 = Color4Extensions.FromHex(@"cc6633");
public readonly Color4 DarkOrange4 = Color4Extensions.FromHex(@"6b422e");
public readonly Color4 Red0 = Color4Extensions.FromHex(@"ff9b9b");
public readonly Color4 Red1 = Color4Extensions.FromHex(@"ff6666");
public readonly Color4 Red2 = Color4Extensions.FromHex(@"eb4747");
+51 -1
View File
@@ -15,15 +15,65 @@ namespace osu.Game.Graphics
/// </summary>
public const float DEFAULT_FONT_SIZE = 16;
/// <summary>
/// Template font styles which should be preferred whenever possible for UI elements.
/// </summary>
public static class Style
{
/// <summary>
/// Equivalent to Torus with 32px size and semi-bold weight.
/// </summary>
public static FontUsage Title => GetFont(Typeface.TorusAlternate, size: 32, weight: FontWeight.Regular);
/// <summary>
/// Torus with 28px size and semi-bold weight.
/// </summary>
public static FontUsage Subtitle => GetFont(size: 28, weight: FontWeight.Regular);
/// <summary>
/// Torus with 22px size and bold weight.
/// </summary>
public static FontUsage Heading1 => GetFont(size: 22, weight: FontWeight.Bold);
/// <summary>
/// Torus with 18px size and semi-bold weight.
/// </summary>
public static FontUsage Heading2 => GetFont(size: 18, weight: FontWeight.SemiBold);
/// <summary>
/// Torus with 16px size and regular weight.
/// </summary>
public static FontUsage Body => GetFont(size: DEFAULT_FONT_SIZE, weight: FontWeight.Regular);
/// <summary>
/// Torus with 14px size and regular weight.
/// </summary>
public static FontUsage Caption1 => GetFont(size: 14, weight: FontWeight.Regular);
/// <summary>
/// Torus with 12px size and regular weight.
/// </summary>
public static FontUsage Caption2 => GetFont(size: 12, weight: FontWeight.Regular);
}
/// <summary>
/// The default font.
/// </summary>
public static FontUsage Default => GetFont();
public static FontUsage Default => GetFont(weight: FontWeight.Medium);
/// <summary>
/// Font face for numeric display.
/// </summary>
public static FontUsage Numeric => GetFont(Typeface.Venera, weight: FontWeight.Bold);
/// <summary>
/// Default font face for UI and game elements.
/// </summary>
public static FontUsage Torus => GetFont(Typeface.Torus, weight: FontWeight.Regular);
/// <summary>
/// Default font face with alternate character set for headings and flair text.
/// </summary>
public static FontUsage TorusAlternate => GetFont(Typeface.TorusAlternate, weight: FontWeight.Regular);
public static FontUsage Inter => GetFont(Typeface.Inter, weight: FontWeight.Regular);
+8
View File
@@ -115,6 +115,7 @@ namespace osu.Game.Graphics
public static IconUsage ChangelogB => get(OsuIconMapping.ChangelogB);
public static IconUsage Chat => get(OsuIconMapping.Chat);
public static IconUsage CheckCircle => get(OsuIconMapping.CheckCircle);
public static IconUsage Clock => get(OsuIconMapping.Clock);
public static IconUsage CollapseA => get(OsuIconMapping.CollapseA);
public static IconUsage Collections => get(OsuIconMapping.Collections);
public static IconUsage Cross => get(OsuIconMapping.Cross);
@@ -141,6 +142,7 @@ namespace osu.Game.Graphics
public static IconUsage Input => get(OsuIconMapping.Input);
public static IconUsage Maintenance => get(OsuIconMapping.Maintenance);
public static IconUsage Megaphone => get(OsuIconMapping.Megaphone);
public static IconUsage Metronome => get(OsuIconMapping.Metronome);
public static IconUsage Music => get(OsuIconMapping.Music);
public static IconUsage News => get(OsuIconMapping.News);
public static IconUsage Next => get(OsuIconMapping.Next);
@@ -204,6 +206,9 @@ namespace osu.Game.Graphics
[Description(@"check-circle")]
CheckCircle,
[Description(@"clock")]
Clock,
[Description(@"collapse-a")]
CollapseA,
@@ -282,6 +287,9 @@ namespace osu.Game.Graphics
[Description(@"megaphone")]
Megaphone,
[Description(@"metronome")]
Metronome,
[Description(@"music")]
Music,
@@ -129,7 +129,7 @@ namespace osu.Game.Graphics.UserInterface
Radius = 5,
},
Colour = ButtonColour,
Shear = new Vector2(0.2f, 0),
Shear = OsuGame.SHEAR,
Children = new Drawable[]
{
new Box
@@ -149,7 +149,7 @@ namespace osu.Game.Graphics.UserInterface
RelativeSizeAxes = Axes.Both,
TriangleScale = 4,
ColourDark = OsuColour.Gray(0.88f),
Shear = new Vector2(-0.2f, 0),
Shear = -OsuGame.SHEAR,
ClampAxes = Axes.Y
},
},
@@ -44,7 +44,8 @@ namespace osu.Game.Graphics.UserInterface
AutoSizeAxes = Axes.Both,
TextAnchor = Anchor.TopRight,
Margin = new MarginPadding { Left = 5, Vertical = 10 },
Text = string.Join('\n', gameHost.Threads.Select(t => t.Name))
Text = string.Join('\n', gameHost.Threads.Select(t => t.Name)),
ParagraphSpacing = 0,
},
textFlow = new OsuTextFlowContainer(cp =>
{
@@ -56,6 +57,7 @@ namespace osu.Game.Graphics.UserInterface
Margin = new MarginPadding { Left = 35, Right = 10, Vertical = 10 },
AutoSizeAxes = Axes.Y,
TextAnchor = Anchor.TopRight,
ParagraphSpacing = 0,
},
};
}
@@ -1,9 +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.Numerics;
using System.Globalization;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@@ -11,7 +9,7 @@ using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Utils;
using osu.Game.Extensions;
namespace osu.Game.Graphics.UserInterface
{
@@ -85,35 +83,6 @@ namespace osu.Game.Graphics.UserInterface
channel.Play();
}
public LocalisableString GetDisplayableValue(T value)
{
if (CurrentNumber.IsInteger)
return int.CreateTruncating(value).ToString("N0");
double floatValue = double.CreateTruncating(value);
decimal decimalPrecision = normalise(decimal.CreateTruncating(CurrentNumber.Precision), max_decimal_digits);
// Find the number of significant digits (we could have less than 5 after normalize())
int significantDigits = FormatUtils.FindPrecision(decimalPrecision);
if (DisplayAsPercentage)
{
return floatValue.ToString($@"P{Math.Max(0, significantDigits - 2)}");
}
string negativeSign = Math.Round(floatValue, significantDigits) < 0 ? "-" : string.Empty;
return $"{negativeSign}{Math.Abs(floatValue).ToString($"N{significantDigits}")}";
}
/// <summary>
/// Removes all non-significant digits, keeping at most a requested number of decimal digits.
/// </summary>
/// <param name="d">The decimal to normalize.</param>
/// <param name="sd">The maximum number of decimal digits to keep. The final result may have fewer decimal digits than this value.</param>
/// <returns>The normalised decimal.</returns>
private decimal normalise(decimal d, int sd)
=> decimal.Parse(Math.Round(d, sd).ToString(string.Concat("0.", new string('#', sd)), CultureInfo.InvariantCulture), CultureInfo.InvariantCulture);
public LocalisableString GetDisplayableValue(T value) => value.ToStandardFormattedString(max_decimal_digits, DisplayAsPercentage);
}
}
@@ -11,7 +11,6 @@ using osu.Framework.Localisation;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Graphics.UserInterface
{
@@ -66,8 +65,6 @@ namespace osu.Game.Graphics.UserInterface
private readonly Box background;
private readonly OsuSpriteText text;
private const float shear = OsuGame.SHEAR;
private Colour4? darkerColour;
private Colour4? lighterColour;
private Colour4? textColour;
@@ -91,10 +88,10 @@ namespace osu.Game.Graphics.UserInterface
public ShearedButton(float? width = null, float height = DEFAULT_HEIGHT)
{
Height = height;
Padding = new MarginPadding { Horizontal = shear * height };
Padding = new MarginPadding { Horizontal = OsuGame.SHEAR.X * height };
Content.CornerRadius = CORNER_RADIUS;
Content.Shear = new Vector2(shear, 0);
Content.Shear = OsuGame.SHEAR;
Content.Masking = true;
Content.Anchor = Content.Origin = Anchor.Centre;
@@ -117,7 +114,7 @@ namespace osu.Game.Graphics.UserInterface
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Shear = new Vector2(-shear, 0),
Shear = -OsuGame.SHEAR,
Child = text = new OsuSpriteText
{
Font = OsuFont.TorusAlternate.With(size: 17),
@@ -26,8 +26,6 @@ namespace osu.Game.Graphics.UserInterface
public const int HEIGHT = 30;
public const float EXPANDED_SIZE = 50;
public static readonly Vector2 SHEAR = new Vector2(0.15f, 0);
private readonly Box fill;
private readonly Container main;
@@ -40,7 +38,7 @@ namespace osu.Game.Graphics.UserInterface
Size = new Vector2(EXPANDED_SIZE, HEIGHT);
InternalChild = main = new Container
{
Shear = SHEAR,
Shear = OsuGame.SHEAR,
BorderColour = Colour4.White,
BorderThickness = BORDER_WIDTH,
Masking = true,
@@ -52,7 +52,7 @@ namespace osu.Game.Graphics.UserInterface
public ShearedSearchTextBox()
{
Height = 42;
Shear = new Vector2(OsuGame.SHEAR, 0);
Shear = OsuGame.SHEAR;
Masking = true;
CornerRadius = corner_radius;
@@ -115,7 +115,7 @@ namespace osu.Game.Graphics.UserInterface
PlaceholderText = CommonStrings.InputSearch;
CornerRadius = corner_radius;
TextContainer.Shear = new Vector2(-OsuGame.SHEAR, 0);
TextContainer.Shear = -OsuGame.SHEAR;
}
protected override SpriteText CreatePlaceholder() => new SearchPlaceholder();
@@ -58,7 +58,7 @@ namespace osu.Game.Graphics.UserInterface
public ShearedSliderBar()
{
Shear = SHEAR;
Shear = OsuGame.SHEAR;
Height = HEIGHT;
RangePadding = EXPANDED_SIZE / 2;
Children = new Drawable[]
@@ -98,11 +98,11 @@ namespace osu.Game.Graphics.UserInterface
},
nubContainer = new Container
{
Shear = -SHEAR,
Shear = -OsuGame.SHEAR,
RelativeSizeAxes = Axes.Both,
Child = Nub = new ShearedNub
{
X = -SHEAR.X * HEIGHT / 2f,
X = -OsuGame.SHEAR.X * HEIGHT / 2f,
Origin = Anchor.TopCentre,
RelativePositionAxes = Axes.X,
Current = { Value = true },
@@ -36,16 +36,6 @@ namespace osu.Game.Graphics.UserInterfaceV2
}
}
protected override void Update()
{
base.Update();
var header = (ShearedDropdownHeader)Header;
var menu = (ShearedDropdownMenu)Menu;
menu.Padding = new MarginPadding { Left = header.LabelContainer.DrawWidth - 10f, Right = 6f };
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Repeat) return false;
@@ -62,18 +52,15 @@ namespace osu.Game.Graphics.UserInterfaceV2
protected partial class ShearedDropdownMenu : OsuDropdown<T>.OsuDropdownMenu
{
private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0);
public new MarginPadding Padding
{
get => base.Padding;
set => base.Padding = value;
}
public ShearedDropdownMenu()
{
Shear = shear;
Shear = OsuGame.SHEAR;
Margin = new MarginPadding { Top = 5f };
Padding = new MarginPadding
{
Left = -6f,
Right = 6f
};
}
protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new ShearedMenuItem(item)
@@ -84,20 +71,16 @@ namespace osu.Game.Graphics.UserInterfaceV2
public partial class ShearedMenuItem : DrawableOsuDropdownMenuItem
{
private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0);
public ShearedMenuItem(MenuItem item)
: base(item)
{
Foreground.Shear = -shear;
Foreground.Shear = -OsuGame.SHEAR;
}
}
}
public partial class ShearedDropdownHeader : DropdownHeader
{
private const float corner_radius = 5f;
private LocalisableString label;
protected override LocalisableString Label
@@ -125,15 +108,13 @@ namespace osu.Game.Graphics.UserInterfaceV2
public ShearedDropdown<T> Dropdown = null!;
private ShearedDropdownSearchBar searchBar = null!;
private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0);
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
public ShearedDropdownHeader()
{
Shear = shear;
CornerRadius = corner_radius;
Shear = OsuGame.SHEAR;
CornerRadius = ShearedButton.CORNER_RADIUS;
Masking = true;
Foreground.Children = new Drawable[]
@@ -154,7 +135,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
LabelContainer = new Container
{
CornerRadius = corner_radius,
Depth = float.MaxValue,
CornerRadius = ShearedButton.CORNER_RADIUS,
Masking = true,
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
@@ -165,9 +147,14 @@ namespace osu.Game.Graphics.UserInterfaceV2
},
labelText = new OsuSpriteText
{
Margin = new MarginPadding { Horizontal = 10f, Vertical = 8f },
Font = OsuFont.Torus.With(size: 16.8f, weight: FontWeight.SemiBold),
Shear = -shear,
Margin = new MarginPadding
{
Horizontal = 10f,
// Chosen specifically so the height of these dropdowns matches ShearedToggleButton (30).
Vertical = 7f
},
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
Shear = -OsuGame.SHEAR,
},
},
},
@@ -178,7 +165,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = 10f },
Shear = -shear,
Shear = -OsuGame.SHEAR,
Children = new Drawable[]
{
valueText = new TruncatingSpriteText
@@ -186,7 +173,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Padding = new MarginPadding { Right = 15f },
Font = OsuFont.Torus.With(size: 16.8f, weight: FontWeight.SemiBold),
Font = OsuFont.Style.Body,
RelativeSizeAxes = Axes.X,
},
chevron = new SpriteIcon
@@ -203,8 +190,6 @@ namespace osu.Game.Graphics.UserInterfaceV2
}
},
};
AddInternal(LabelContainer.CreateProxy());
}
[BackgroundDependencyLoader]
@@ -229,7 +214,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
searchBar.Padding = new MarginPadding { Left = LabelContainer.DrawWidth };
// By limiting the width we avoid this box showing up as an outline around the drawables that are on top of it.
Background.Padding = new MarginPadding { Left = LabelContainer.DrawWidth - corner_radius };
Background.Padding = new MarginPadding { Left = LabelContainer.DrawWidth - ShearedButton.CORNER_RADIUS };
}
protected override bool OnHover(HoverEvent e)
@@ -286,12 +271,10 @@ namespace osu.Game.Graphics.UserInterfaceV2
private partial class DropdownSearchTextBox : OsuTextBox
{
private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0);
[BackgroundDependencyLoader]
private void load(OverlayColourProvider? colourProvider)
{
TextContainer.Shear = -shear;
TextContainer.Shear = -OsuGame.SHEAR;
BackgroundUnfocused = colourProvider?.Background5 ?? new Color4(10, 10, 10, 255);
BackgroundFocused = colourProvider?.Background5 ?? new Color4(10, 10, 10, 255);
}

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