mirror of
https://github.com/ppy/osu.git
synced 2026-05-25 17:49:57 +08:00
Merge branch 'master' into song-select-v2-wedges-metadata
This commit is contained in:
+1
-1
@@ -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.
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,10 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
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]
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,10 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
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]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,10 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
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]
|
||||
|
||||
@@ -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%"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
|
||||
{
|
||||
@@ -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,17 +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;
|
||||
@@ -66,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,17 +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;
|
||||
@@ -66,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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ namespace osu.Game.Graphics.Carousel
|
||||
/// </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.Graphics.Carousel
|
||||
/// </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;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString SuggestedOffsetNote => new TranslatableString(getKey(@"suggested_offset_note"), @"Play a few beatmaps to receive a suggested offset!");
|
||||
|
||||
/// <summary>
|
||||
/// "Based on the last {0} play(s), your offset is set correctly!"
|
||||
/// </summary>
|
||||
public static LocalisableString SuggestedOffsetCorrect(int plays) => new TranslatableString(getKey(@"suggested_offset_correct"), @"Based on the last {0} play(s), your offset is set correctly!", plays);
|
||||
|
||||
/// <summary>
|
||||
/// "Based on the last {0} play(s), the suggested offset is {1} ms."
|
||||
/// </summary>
|
||||
|
||||
@@ -54,6 +54,9 @@ namespace osu.Game.Online.Leaderboards
|
||||
lastFetchCompletionSource?.TrySetCanceled();
|
||||
scores.Value = null;
|
||||
|
||||
if (newCriteria.Beatmap == null || newCriteria.Ruleset == null)
|
||||
return Task.FromResult<LeaderboardScores?>(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NoneSelected));
|
||||
|
||||
switch (newCriteria.Scope)
|
||||
{
|
||||
case BeatmapLeaderboardScope.Local:
|
||||
@@ -72,6 +75,21 @@ namespace osu.Game.Online.Leaderboards
|
||||
|
||||
default:
|
||||
{
|
||||
if (!api.IsLoggedIn)
|
||||
return Task.FromResult<LeaderboardScores?>(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NotLoggedIn));
|
||||
|
||||
if (!newCriteria.Ruleset.IsLegacyRuleset())
|
||||
return Task.FromResult<LeaderboardScores?>(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.RulesetUnavailable));
|
||||
|
||||
if (newCriteria.Beatmap.OnlineID <= 0 || newCriteria.Beatmap.Status <= BeatmapOnlineStatus.Pending)
|
||||
return Task.FromResult<LeaderboardScores?>(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.BeatmapUnavailable));
|
||||
|
||||
if ((newCriteria.Scope.RequiresSupporter(newCriteria.ExactMods != null)) && !api.LocalUser.Value.IsSupporter)
|
||||
return Task.FromResult<LeaderboardScores?>(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NotSupporter));
|
||||
|
||||
if (newCriteria.Scope == BeatmapLeaderboardScope.Team && api.LocalUser.Value.Team == null)
|
||||
return Task.FromResult<LeaderboardScores?>(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NoTeam));
|
||||
|
||||
var onlineFetchCompletionSource = new TaskCompletionSource<LeaderboardScores?>();
|
||||
lastFetchCompletionSource = onlineFetchCompletionSource;
|
||||
|
||||
@@ -92,16 +110,16 @@ namespace osu.Game.Online.Leaderboards
|
||||
if (inFlightOnlineRequest != null && !newRequest.Equals(inFlightOnlineRequest))
|
||||
return;
|
||||
|
||||
var result = new LeaderboardScores
|
||||
var result = LeaderboardScores.Success
|
||||
(
|
||||
response.Scores.Select(s => s.ToScoreInfo(rulesets, newCriteria.Beatmap)).OrderByTotalScore(),
|
||||
response.Scores.Select(s => s.ToScoreInfo(rulesets, newCriteria.Beatmap)).OrderByTotalScore().ToArray(),
|
||||
response.UserScore?.CreateScoreInfo(rulesets, newCriteria.Beatmap)
|
||||
);
|
||||
inFlightOnlineRequest = null;
|
||||
if (onlineFetchCompletionSource.TrySetResult(result))
|
||||
scores.Value = result;
|
||||
};
|
||||
newRequest.Failure += ex => onlineFetchCompletionSource.TrySetException(ex);
|
||||
newRequest.Failure += _ => onlineFetchCompletionSource.TrySetResult(LeaderboardScores.Failure(LeaderboardFailState.NetworkFailure));
|
||||
api.Queue(inFlightOnlineRequest = newRequest);
|
||||
return onlineFetchCompletionSource.Task;
|
||||
}
|
||||
@@ -138,7 +156,7 @@ namespace osu.Game.Online.Leaderboards
|
||||
|
||||
newScores = newScores.Detach().OrderByTotalScore();
|
||||
|
||||
scores.Value = new LeaderboardScores(newScores, null);
|
||||
scores.Value = LeaderboardScores.Success(newScores.ToArray(), null);
|
||||
|
||||
if (localFetchCompletionSource != null && localFetchCompletionSource == lastFetchCompletionSource)
|
||||
{
|
||||
@@ -149,14 +167,18 @@ namespace osu.Game.Online.Leaderboards
|
||||
}
|
||||
|
||||
public record LeaderboardCriteria(
|
||||
BeatmapInfo Beatmap,
|
||||
RulesetInfo Ruleset,
|
||||
BeatmapInfo? Beatmap,
|
||||
RulesetInfo? Ruleset,
|
||||
BeatmapLeaderboardScope Scope,
|
||||
Mod[]? ExactMods
|
||||
);
|
||||
|
||||
public record LeaderboardScores(IEnumerable<ScoreInfo> TopScores, ScoreInfo? UserScore)
|
||||
public record LeaderboardScores
|
||||
{
|
||||
public ICollection<ScoreInfo> TopScores { get; }
|
||||
public ScoreInfo? UserScore { get; }
|
||||
public LeaderboardFailState? FailState { get; }
|
||||
|
||||
public IEnumerable<ScoreInfo> AllScores
|
||||
{
|
||||
get
|
||||
@@ -168,5 +190,26 @@ namespace osu.Game.Online.Leaderboards
|
||||
yield return UserScore;
|
||||
}
|
||||
}
|
||||
|
||||
private LeaderboardScores(ICollection<ScoreInfo> topScores, ScoreInfo? userScore, LeaderboardFailState? failState)
|
||||
{
|
||||
TopScores = topScores;
|
||||
UserScore = userScore;
|
||||
FailState = failState;
|
||||
}
|
||||
|
||||
public static LeaderboardScores Success(ICollection<ScoreInfo> topScores, ScoreInfo? userScore) => new LeaderboardScores(topScores, userScore, null);
|
||||
public static LeaderboardScores Failure(LeaderboardFailState failState) => new LeaderboardScores([], null, failState);
|
||||
}
|
||||
|
||||
public enum LeaderboardFailState
|
||||
{
|
||||
NetworkFailure = -1,
|
||||
BeatmapUnavailable = -2,
|
||||
RulesetUnavailable = -3,
|
||||
NoneSelected = -4,
|
||||
NotLoggedIn = -5,
|
||||
NotSupporter = -6,
|
||||
NoTeam = -7
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,13 +7,14 @@ namespace osu.Game.Online.Leaderboards
|
||||
{
|
||||
Success,
|
||||
Retrieving,
|
||||
NetworkFailure,
|
||||
BeatmapUnavailable,
|
||||
RulesetUnavailable,
|
||||
NoneSelected,
|
||||
NoScores,
|
||||
NotLoggedIn,
|
||||
NotSupporter,
|
||||
NoTeam
|
||||
|
||||
NetworkFailure = LeaderboardFailState.NetworkFailure,
|
||||
BeatmapUnavailable = LeaderboardFailState.BeatmapUnavailable,
|
||||
RulesetUnavailable = LeaderboardFailState.RulesetUnavailable,
|
||||
NoneSelected = LeaderboardFailState.NoneSelected,
|
||||
NotLoggedIn = LeaderboardFailState.NotLoggedIn,
|
||||
NotSupporter = LeaderboardFailState.NotSupporter,
|
||||
NoTeam = LeaderboardFailState.NoTeam,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace osu.Game.Overlays.Music
|
||||
/// <summary>
|
||||
/// A <see cref="CollectionDropdown"/> for use in the <see cref="NowPlayingOverlay"/>.
|
||||
/// </summary>
|
||||
public partial class NowPlayingCollectionDropdown : CollectionDropdown
|
||||
public partial class NowPlayingCollectionDropdown : CollectionDropdown // TODO: class is now unused. if we decide this isn't coming back it can be nuked.
|
||||
{
|
||||
protected override bool ShowManageCollectionsItem => false;
|
||||
|
||||
|
||||
@@ -8,13 +8,13 @@ using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@@ -109,6 +109,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
|
||||
base.LoadComplete();
|
||||
|
||||
averageHitErrorHistory.BindCollectionChanged(updateDisplay, true);
|
||||
current.BindValueChanged(_ => updateHintText());
|
||||
SuggestedOffset.BindValueChanged(_ => updateHintText(), true);
|
||||
}
|
||||
|
||||
@@ -148,17 +149,28 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
|
||||
break;
|
||||
}
|
||||
|
||||
SuggestedOffset.Value = averageHitErrorHistory.Any() ? averageHitErrorHistory.Average(dataPoint => dataPoint.SuggestedGlobalAudioOffset) : null;
|
||||
SuggestedOffset.Value = averageHitErrorHistory.Any() ? Math.Round(averageHitErrorHistory.Average(dataPoint => dataPoint.SuggestedGlobalAudioOffset)) : null;
|
||||
}
|
||||
|
||||
private float getXPositionForOffset(double offset) => (float)(Math.Clamp(offset, current.MinValue, current.MaxValue) / (2 * current.MaxValue));
|
||||
|
||||
private void updateHintText()
|
||||
{
|
||||
hintText.Text = SuggestedOffset.Value == null
|
||||
? AudioSettingsStrings.SuggestedOffsetNote
|
||||
: AudioSettingsStrings.SuggestedOffsetValueReceived(averageHitErrorHistory.Count, SuggestedOffset.Value.ToLocalisableString(@"N0"));
|
||||
applySuggestion.Enabled.Value = SuggestedOffset.Value != null;
|
||||
if (SuggestedOffset.Value == null)
|
||||
{
|
||||
applySuggestion.Enabled.Value = false;
|
||||
hintText.Text = AudioSettingsStrings.SuggestedOffsetNote;
|
||||
}
|
||||
else if (Math.Abs(SuggestedOffset.Value.Value - current.Value) < 1)
|
||||
{
|
||||
applySuggestion.Enabled.Value = false;
|
||||
hintText.Text = AudioSettingsStrings.SuggestedOffsetCorrect(averageHitErrorHistory.Count);
|
||||
}
|
||||
else
|
||||
{
|
||||
applySuggestion.Enabled.Value = true;
|
||||
hintText.Text = AudioSettingsStrings.SuggestedOffsetValueReceived(averageHitErrorHistory.Count, SuggestedOffset.Value.Value.ToStandardFormattedString(0, false));
|
||||
}
|
||||
}
|
||||
|
||||
private partial class OffsetSliderBar : RoundedSliderBar<double>
|
||||
|
||||
@@ -34,5 +34,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
yield return ("Speed change", $"{SpeedChange.Value:N2}x");
|
||||
}
|
||||
}
|
||||
|
||||
public override string ExtendedIconInformation => SpeedChange.IsDefault ? string.Empty : FormattableString.Invariant($"{SpeedChange.Value:N2}x");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1266,7 +1266,7 @@ namespace osu.Game.Screens.Edit
|
||||
yield return createDifficultyCreationMenu();
|
||||
yield return createDifficultySwitchMenu();
|
||||
yield return new OsuMenuItemSpacer();
|
||||
yield return new EditorMenuItem(EditorStrings.DeleteDifficulty, MenuItemType.Standard, deleteDifficulty) { Action = { Disabled = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count < 2 } };
|
||||
yield return new EditorMenuItem(EditorStrings.DeleteDifficulty, MenuItemType.Destructive, deleteDifficulty) { Action = { Disabled = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count < 2 } };
|
||||
yield return new OsuMenuItemSpacer();
|
||||
|
||||
var save = new EditorMenuItem(WebCommonStrings.ButtonsSave, MenuItemType.Standard, () => attemptMutationOperation(Save)) { Hotkey = new Hotkey(PlatformAction.Save) };
|
||||
|
||||
@@ -31,14 +31,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
private readonly Bindable<bool> expandedFromTextBoxFocus = new Bindable<bool>();
|
||||
|
||||
private const float height = 100;
|
||||
private const float width = 260;
|
||||
|
||||
public override bool PropagateNonPositionalInputSubTree => true;
|
||||
|
||||
public GameplayChatDisplay(Room room)
|
||||
: base(room, leaveChannelOnDispose: false)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Background.Alpha = 0.2f;
|
||||
Width = width;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
||||
@@ -15,8 +15,8 @@ using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
@@ -25,6 +25,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
{
|
||||
protected override bool PauseOnFocusLost => false;
|
||||
|
||||
protected override bool ShowLeaderboard => true;
|
||||
|
||||
protected override UserActivity InitialActivity => new UserActivity.InMultiplayerGame(Beatmap.Value.BeatmapInfo, Ruleset.Value);
|
||||
|
||||
[Resolved]
|
||||
@@ -33,10 +35,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
private IBindable<bool> isConnected = null!;
|
||||
|
||||
private readonly TaskCompletionSource<bool> resultsReady = new TaskCompletionSource<bool>();
|
||||
private readonly MultiplayerRoomUser[] users;
|
||||
|
||||
private LoadingLayer loadingDisplay = null!;
|
||||
private MultiplayerGameplayLeaderboard multiplayerLeaderboard = null!;
|
||||
|
||||
[Cached(typeof(IGameplayLeaderboardProvider))]
|
||||
private readonly MultiplayerLeaderboardProvider leaderboardProvider;
|
||||
|
||||
private GameplayMatchScoreDisplay teamScoreDisplay = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a multiplayer player.
|
||||
@@ -55,7 +60,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
AlwaysShowLeaderboard = true,
|
||||
})
|
||||
{
|
||||
this.users = users;
|
||||
leaderboardProvider = new MultiplayerLeaderboardProvider(users);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@@ -71,26 +76,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
Expanded = { BindTarget = LeaderboardExpandedState },
|
||||
}, chat => HUDOverlay.LeaderboardFlow.Insert(2, chat));
|
||||
|
||||
HUDOverlay.Add(loadingDisplay = new LoadingLayer(true) { Depth = float.MaxValue });
|
||||
}
|
||||
|
||||
protected override GameplayLeaderboard CreateGameplayLeaderboard() => multiplayerLeaderboard = new MultiplayerGameplayLeaderboard(users);
|
||||
|
||||
protected override void AddLeaderboardToHUD(GameplayLeaderboard leaderboard)
|
||||
{
|
||||
Debug.Assert(leaderboard == multiplayerLeaderboard);
|
||||
|
||||
HUDOverlay.LeaderboardFlow.Insert(0, leaderboard);
|
||||
|
||||
if (multiplayerLeaderboard.TeamScores.Count >= 2)
|
||||
LoadComponentAsync(teamScoreDisplay = new GameplayMatchScoreDisplay
|
||||
{
|
||||
LoadComponentAsync(new GameplayMatchScoreDisplay
|
||||
Expanded = { BindTarget = HUDOverlay.ShowHud },
|
||||
Alpha = 0,
|
||||
}, scoreDisplay => HUDOverlay.LeaderboardFlow.Insert(1, scoreDisplay));
|
||||
LoadComponentAsync(leaderboardProvider, loaded =>
|
||||
{
|
||||
AddInternal(loaded);
|
||||
|
||||
if (loaded.HasTeams)
|
||||
{
|
||||
Team1Score = { BindTarget = multiplayerLeaderboard.TeamScores.First().Value },
|
||||
Team2Score = { BindTarget = multiplayerLeaderboard.TeamScores.Last().Value },
|
||||
Expanded = { BindTarget = HUDOverlay.ShowHud },
|
||||
}, scoreDisplay => HUDOverlay.LeaderboardFlow.Insert(1, scoreDisplay));
|
||||
}
|
||||
teamScoreDisplay.Alpha = 1;
|
||||
teamScoreDisplay.Team1Score.BindTarget = leaderboardProvider.TeamScores.First().Value;
|
||||
teamScoreDisplay.Team2Score.BindTarget = leaderboardProvider.TeamScores.Last().Value;
|
||||
}
|
||||
});
|
||||
|
||||
HUDOverlay.Add(loadingDisplay = new LoadingLayer(true) { Depth = float.MaxValue });
|
||||
}
|
||||
|
||||
protected override void LoadAsyncComplete()
|
||||
@@ -195,8 +198,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
{
|
||||
Debug.Assert(Room.RoomID != null);
|
||||
|
||||
return multiplayerLeaderboard.TeamScores.Count == 2
|
||||
? new MultiplayerTeamResultsScreen(score, Room.RoomID.Value, PlaylistItem, multiplayerLeaderboard.TeamScores)
|
||||
return leaderboardProvider.TeamScores.Count == 2
|
||||
? new MultiplayerTeamResultsScreen(score, Room.RoomID.Value, PlaylistItem, leaderboardProvider.TeamScores)
|
||||
{
|
||||
IsLocalPlay = true,
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ using osu.Game.Online.Rooms;
|
||||
using osu.Game.Online.Spectator;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osu.Game.Screens.Spectate;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
@@ -47,17 +48,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
[Resolved]
|
||||
private MultiplayerClient multiplayerClient { get; set; } = null!;
|
||||
|
||||
[Cached(typeof(IGameplayLeaderboardProvider))]
|
||||
private MultiSpectatorLeaderboardProvider leaderboardProvider { get; set; }
|
||||
|
||||
private IAggregateAudioAdjustment? boundAdjustments;
|
||||
|
||||
private readonly PlayerArea[] instances;
|
||||
private MasterGameplayClockContainer masterClockContainer = null!;
|
||||
private SpectatorSyncManager syncManager = null!;
|
||||
private PlayerGrid grid = null!;
|
||||
private MultiSpectatorLeaderboard leaderboard = null!;
|
||||
private PlayerArea? currentAudioSource;
|
||||
|
||||
private readonly Room room;
|
||||
private readonly MultiplayerRoomUser[] users;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="MultiSpectatorScreen"/>.
|
||||
@@ -68,9 +70,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
: base(users.Select(u => u.UserID).ToArray())
|
||||
{
|
||||
this.room = room;
|
||||
this.users = users;
|
||||
|
||||
instances = new PlayerArea[Users.Count];
|
||||
leaderboardProvider = new MultiSpectatorLeaderboardProvider(users);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@@ -133,25 +135,25 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
for (int i = 0; i < Users.Count; i++)
|
||||
grid.Add(instances[i] = new PlayerArea(Users[i], syncManager.CreateManagedClock()));
|
||||
|
||||
LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(users)
|
||||
{
|
||||
Expanded = { Value = true },
|
||||
}, _ =>
|
||||
LoadComponentAsync(leaderboardProvider, _ =>
|
||||
{
|
||||
AddInternal(leaderboardProvider);
|
||||
foreach (var instance in instances)
|
||||
leaderboard.AddClock(instance.UserId, instance.SpectatorPlayerClock);
|
||||
leaderboardProvider.AddClock(instance.UserId, instance.SpectatorPlayerClock);
|
||||
|
||||
leaderboardFlow.Insert(0, leaderboard);
|
||||
|
||||
if (leaderboard.TeamScores.Count == 2)
|
||||
if (leaderboardProvider.TeamScores.Count == 2)
|
||||
{
|
||||
LoadComponentAsync(new MatchScoreDisplay
|
||||
{
|
||||
Team1Score = { BindTarget = leaderboard.TeamScores.First().Value },
|
||||
Team2Score = { BindTarget = leaderboard.TeamScores.Last().Value },
|
||||
Team1Score = { BindTarget = leaderboardProvider.TeamScores.First().Value },
|
||||
Team2Score = { BindTarget = leaderboardProvider.TeamScores.Last().Value },
|
||||
}, scoreDisplayContainer.Add);
|
||||
}
|
||||
});
|
||||
leaderboardFlow.Insert(0, new DrawableGameplayLeaderboard
|
||||
{
|
||||
Expanded = { Value = true }
|
||||
});
|
||||
|
||||
LoadComponentAsync(new GameplayChatDisplay(room)
|
||||
{
|
||||
|
||||
+44
-26
@@ -3,40 +3,48 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Users;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public abstract partial class GameplayLeaderboard : CompositeDrawable
|
||||
public partial class DrawableGameplayLeaderboard : CompositeDrawable
|
||||
{
|
||||
private readonly Cached sorting = new Cached();
|
||||
|
||||
public Bindable<bool> Expanded = new Bindable<bool>();
|
||||
|
||||
protected readonly FillFlowContainer<GameplayLeaderboardScore> Flow;
|
||||
protected readonly FillFlowContainer<DrawableGameplayLeaderboardScore> Flow;
|
||||
|
||||
private bool requiresScroll;
|
||||
private readonly OsuScrollContainer scroll;
|
||||
|
||||
public GameplayLeaderboardScore? TrackedScore { get; private set; }
|
||||
public DrawableGameplayLeaderboardScore? TrackedScore { get; private set; }
|
||||
|
||||
[Resolved]
|
||||
private IGameplayLeaderboardProvider? leaderboardProvider { get; set; }
|
||||
|
||||
private readonly IBindableList<GameplayLeaderboardScore> scores = new BindableList<GameplayLeaderboardScore>();
|
||||
private readonly Bindable<bool> configVisibility = new Bindable<bool>();
|
||||
|
||||
private const int max_panels = 8;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new leaderboard.
|
||||
/// </summary>
|
||||
protected GameplayLeaderboard()
|
||||
public DrawableGameplayLeaderboard()
|
||||
{
|
||||
Width = GameplayLeaderboardScore.EXTENDED_WIDTH + GameplayLeaderboardScore.SHEAR_WIDTH;
|
||||
Width = DrawableGameplayLeaderboardScore.EXTENDED_WIDTH + DrawableGameplayLeaderboardScore.SHEAR_WIDTH;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
@@ -44,10 +52,10 @@ namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
ClampExtension = 0,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = Flow = new FillFlowContainer<GameplayLeaderboardScore>
|
||||
Child = Flow = new FillFlowContainer<DrawableGameplayLeaderboardScore>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
X = GameplayLeaderboardScore.SHEAR_WIDTH,
|
||||
X = DrawableGameplayLeaderboardScore.SHEAR_WIDTH,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(2.5f),
|
||||
@@ -58,26 +66,39 @@ namespace osu.Game.Screens.Play.HUD
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
config.BindWith(OsuSetting.GameplayLeaderboard, configVisibility);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
if (leaderboardProvider != null)
|
||||
{
|
||||
scores.BindTo(leaderboardProvider.Scores);
|
||||
scores.BindCollectionChanged((_, _) =>
|
||||
{
|
||||
Clear();
|
||||
foreach (var score in scores)
|
||||
Add(score);
|
||||
}, true);
|
||||
}
|
||||
|
||||
Scheduler.AddDelayed(sort, 1000, true);
|
||||
configVisibility.BindValueChanged(_ => this.FadeTo(configVisibility.Value ? 1 : 0, 100, Easing.OutQuint), true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a player to the leaderboard.
|
||||
/// </summary>
|
||||
/// <param name="user">The player.</param>
|
||||
/// <param name="isTracked">
|
||||
/// Whether the player should be tracked on the leaderboard.
|
||||
/// Set to <c>true</c> for the local player or a player whose replay is currently being played.
|
||||
/// </param>
|
||||
public ILeaderboardScore Add(IUser? user, bool isTracked)
|
||||
public void Add(GameplayLeaderboardScore score)
|
||||
{
|
||||
var drawable = CreateLeaderboardScoreDrawable(user, isTracked);
|
||||
var drawable = CreateLeaderboardScoreDrawable(score);
|
||||
|
||||
if (isTracked)
|
||||
if (score.Tracked)
|
||||
{
|
||||
if (TrackedScore != null)
|
||||
throw new InvalidOperationException("Cannot track more than one score.");
|
||||
@@ -92,10 +113,8 @@ namespace osu.Game.Screens.Play.HUD
|
||||
drawable.DisplayOrder.BindValueChanged(_ => sorting.Invalidate(), true);
|
||||
|
||||
int displayCount = Math.Min(Flow.Count, max_panels);
|
||||
Height = displayCount * (GameplayLeaderboardScore.PANEL_HEIGHT + Flow.Spacing.Y);
|
||||
Height = displayCount * (DrawableGameplayLeaderboardScore.PANEL_HEIGHT + Flow.Spacing.Y);
|
||||
requiresScroll = displayCount != Flow.Count;
|
||||
|
||||
return drawable;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
@@ -105,8 +124,8 @@ namespace osu.Game.Screens.Play.HUD
|
||||
scroll.ScrollToStart(false);
|
||||
}
|
||||
|
||||
protected virtual GameplayLeaderboardScore CreateLeaderboardScoreDrawable(IUser? user, bool isTracked) =>
|
||||
new GameplayLeaderboardScore(user, isTracked);
|
||||
protected virtual DrawableGameplayLeaderboardScore CreateLeaderboardScoreDrawable(GameplayLeaderboardScore score) =>
|
||||
new DrawableGameplayLeaderboardScore(score);
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
@@ -119,7 +138,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
scroll.ScrollTo(scrollTarget);
|
||||
}
|
||||
|
||||
const float panel_height = GameplayLeaderboardScore.PANEL_HEIGHT;
|
||||
const float panel_height = DrawableGameplayLeaderboardScore.PANEL_HEIGHT;
|
||||
|
||||
float fadeBottom = (float)(scroll.Current + scroll.DrawHeight);
|
||||
float fadeTop = (float)(scroll.Current + panel_height);
|
||||
@@ -170,15 +189,14 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
for (int i = 0; i < Flow.Count; i++)
|
||||
{
|
||||
Flow.SetLayoutPosition(orderedByScore[i], i);
|
||||
orderedByScore[i].ScorePosition = CheckValidScorePosition(orderedByScore[i], i + 1) ? i + 1 : null;
|
||||
var score = orderedByScore[i];
|
||||
Flow.SetLayoutPosition(score, i);
|
||||
score.ScorePosition = i + 1 == Flow.Count && leaderboardProvider?.IsPartial == true && score.Tracked ? null : i + 1;
|
||||
}
|
||||
|
||||
sorting.Validate();
|
||||
}
|
||||
|
||||
protected virtual bool CheckValidScorePosition(GameplayLeaderboardScore score, int position) => true;
|
||||
|
||||
private partial class InputDisabledScrollContainer : OsuScrollContainer
|
||||
{
|
||||
public InputDisabledScrollContainer()
|
||||
+18
-9
@@ -14,6 +14,7 @@ using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osu.Game.Users;
|
||||
using osu.Game.Users.Drawables;
|
||||
using osu.Game.Utils;
|
||||
@@ -22,7 +23,7 @@ using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public partial class GameplayLeaderboardScore : CompositeDrawable, ILeaderboardScore
|
||||
public partial class DrawableGameplayLeaderboardScore : CompositeDrawable
|
||||
{
|
||||
public const float EXTENDED_WIDTH = regular_width + top_player_left_width_extension;
|
||||
|
||||
@@ -112,19 +113,27 @@ namespace osu.Game.Screens.Play.HUD
|
||||
private bool isFriend;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="GameplayLeaderboardScore"/>.
|
||||
/// Creates a new <see cref="DrawableGameplayLeaderboardScore"/>.
|
||||
/// </summary>
|
||||
/// <param name="user">The score's player.</param>
|
||||
/// <param name="tracked">Whether the player is the local user or a replay player.</param>
|
||||
public GameplayLeaderboardScore(IUser? user, bool tracked)
|
||||
public DrawableGameplayLeaderboardScore(GameplayLeaderboardScore score)
|
||||
{
|
||||
User = user;
|
||||
Tracked = tracked;
|
||||
User = score.User;
|
||||
Tracked = score.Tracked;
|
||||
TotalScore.BindTo(score.TotalScore);
|
||||
Accuracy.BindTo(score.Accuracy);
|
||||
Combo.BindTo(score.Combo);
|
||||
HasQuit.BindTo(score.HasQuit);
|
||||
DisplayOrder.BindTo(score.DisplayOrder);
|
||||
GetDisplayScore = score.GetDisplayScore;
|
||||
|
||||
if (score.TeamColour != null)
|
||||
{
|
||||
BackgroundColour = score.TeamColour.Value;
|
||||
TextColour = Color4.White;
|
||||
}
|
||||
|
||||
AutoSizeAxes = Axes.X;
|
||||
Height = PANEL_HEIGHT;
|
||||
|
||||
GetDisplayScore = _ => TotalScore.Value;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@@ -1,31 +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;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public interface ILeaderboardScore
|
||||
{
|
||||
BindableLong TotalScore { get; }
|
||||
BindableDouble Accuracy { get; }
|
||||
BindableInt Combo { get; }
|
||||
|
||||
BindableBool HasQuit { get; }
|
||||
|
||||
/// <summary>
|
||||
/// An optional value to guarantee stable ordering.
|
||||
/// Lower numbers will appear higher in cases of <see cref="TotalScore"/> ties.
|
||||
/// </summary>
|
||||
Bindable<long> DisplayOrder { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A custom function which handles converting a score to a display score using a provide <see cref="ScoringMode"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If no function is provided, <see cref="TotalScore"/> will be used verbatim.</remarks>
|
||||
Func<ScoringMode, long> GetDisplayScore { set; }
|
||||
}
|
||||
}
|
||||
@@ -1,108 +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 osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public partial class SoloGameplayLeaderboard : GameplayLeaderboard
|
||||
{
|
||||
private const int duration = 100;
|
||||
|
||||
private readonly Bindable<bool> configVisibility = new Bindable<bool>();
|
||||
|
||||
private readonly Bindable<PlayBeatmapDetailArea.TabType> scoreSource = new Bindable<PlayBeatmapDetailArea.TabType>();
|
||||
|
||||
private readonly IUser trackingUser;
|
||||
|
||||
public readonly IBindableList<ScoreInfo> Scores = new BindableList<ScoreInfo>();
|
||||
|
||||
[Resolved]
|
||||
private ScoreProcessor scoreProcessor { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the leaderboard should be visible regardless of the configuration value.
|
||||
/// This is true by default, but can be changed.
|
||||
/// </summary>
|
||||
public readonly Bindable<bool> AlwaysVisible = new Bindable<bool>(true);
|
||||
|
||||
public SoloGameplayLeaderboard(IUser trackingUser)
|
||||
{
|
||||
this.trackingUser = trackingUser;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
config.BindWith(OsuSetting.GameplayLeaderboard, configVisibility);
|
||||
config.BindWith(OsuSetting.BeatmapDetailTab, scoreSource);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Scores.BindCollectionChanged((_, _) => Scheduler.AddOnce(showScores), true);
|
||||
|
||||
// Alpha will be updated via `updateVisibility` below.
|
||||
Alpha = 0;
|
||||
|
||||
AlwaysVisible.BindValueChanged(_ => updateVisibility());
|
||||
configVisibility.BindValueChanged(_ => updateVisibility(), true);
|
||||
}
|
||||
|
||||
private void showScores()
|
||||
{
|
||||
Clear();
|
||||
|
||||
if (!Scores.Any())
|
||||
return;
|
||||
|
||||
foreach (var s in Scores)
|
||||
{
|
||||
var score = Add(s.User, false);
|
||||
|
||||
score.GetDisplayScore = s.GetDisplayScore;
|
||||
score.TotalScore.Value = s.TotalScore;
|
||||
score.Accuracy.Value = s.Accuracy;
|
||||
score.Combo.Value = s.MaxCombo;
|
||||
score.DisplayOrder.Value = s.OnlineID > 0 ? s.OnlineID : s.Date.ToUnixTimeSeconds();
|
||||
}
|
||||
|
||||
ILeaderboardScore local = Add(trackingUser, true);
|
||||
|
||||
local.GetDisplayScore = scoreProcessor.GetDisplayScore;
|
||||
local.TotalScore.BindTarget = scoreProcessor.TotalScore;
|
||||
local.Accuracy.BindTarget = scoreProcessor.Accuracy;
|
||||
local.Combo.BindTarget = scoreProcessor.HighestCombo;
|
||||
|
||||
// Local score should always show lower than any existing scores in cases of ties.
|
||||
local.DisplayOrder.Value = long.MaxValue;
|
||||
}
|
||||
|
||||
protected override bool CheckValidScorePosition(GameplayLeaderboardScore score, int position)
|
||||
{
|
||||
// change displayed position to '-' when there are 50 already submitted scores and tracked score is last
|
||||
if (score.Tracked && scoreSource.Value != PlayBeatmapDetailArea.TabType.Local)
|
||||
{
|
||||
if (position == Flow.Count && Flow.Count > GetScoresRequest.MAX_SCORES_PER_REQUEST)
|
||||
return false;
|
||||
}
|
||||
|
||||
return base.CheckValidScorePosition(score, position);
|
||||
}
|
||||
|
||||
private void updateVisibility() =>
|
||||
this.FadeTo(AlwaysVisible.Value || configVisibility.Value ? 1 : 0, duration);
|
||||
}
|
||||
}
|
||||
@@ -935,34 +935,30 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
#region Gameplay leaderboard
|
||||
|
||||
protected virtual bool ShowLeaderboard => false;
|
||||
|
||||
protected readonly Bindable<bool> LeaderboardExpandedState = new BindableBool();
|
||||
|
||||
private void loadLeaderboard()
|
||||
{
|
||||
if (!ShowLeaderboard)
|
||||
return;
|
||||
|
||||
HUDOverlay.HoldingForHUD.BindValueChanged(_ => updateLeaderboardExpandedState());
|
||||
LocalUserPlaying.BindValueChanged(_ => updateLeaderboardExpandedState(), true);
|
||||
|
||||
var gameplayLeaderboard = CreateGameplayLeaderboard();
|
||||
|
||||
if (gameplayLeaderboard != null)
|
||||
var gameplayLeaderboard = new DrawableGameplayLeaderboard();
|
||||
LoadComponentAsync(gameplayLeaderboard, leaderboard =>
|
||||
{
|
||||
LoadComponentAsync(gameplayLeaderboard, leaderboard =>
|
||||
{
|
||||
if (!LoadedBeatmapSuccessfully)
|
||||
return;
|
||||
if (!LoadedBeatmapSuccessfully)
|
||||
return;
|
||||
|
||||
leaderboard.Expanded.BindTo(LeaderboardExpandedState);
|
||||
leaderboard.Expanded.BindTo(LeaderboardExpandedState);
|
||||
|
||||
AddLeaderboardToHUD(leaderboard);
|
||||
});
|
||||
}
|
||||
HUDOverlay.LeaderboardFlow.Add(leaderboard);
|
||||
});
|
||||
}
|
||||
|
||||
[CanBeNull]
|
||||
protected virtual GameplayLeaderboard CreateGameplayLeaderboard() => null;
|
||||
|
||||
protected virtual void AddLeaderboardToHUD(GameplayLeaderboard leaderboard) => HUDOverlay.LeaderboardFlow.Add(leaderboard);
|
||||
|
||||
private void updateLeaderboardExpandedState() =>
|
||||
LeaderboardExpandedState.Value = !LocalUserPlaying.Value || HUDOverlay.HoldingForHUD.Value;
|
||||
|
||||
|
||||
@@ -8,18 +8,16 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Play.PlayerSettings;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
@@ -35,6 +33,9 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
private PlaybackSettings playbackSettings;
|
||||
|
||||
[Cached(typeof(IGameplayLeaderboardProvider))]
|
||||
private readonly SoloGameplayLeaderboardProvider leaderboardProvider = new SoloGameplayLeaderboardProvider();
|
||||
|
||||
protected override UserActivity InitialActivity => new UserActivity.WatchingReplay(Score.ScoreInfo);
|
||||
|
||||
private bool isAutoplayPlayback => GameplayState.Mods.OfType<ModAutoplay>().Any();
|
||||
@@ -48,6 +49,8 @@ namespace osu.Game.Screens.Play
|
||||
return base.CheckModsAllowFailure();
|
||||
}
|
||||
|
||||
protected override bool ShowLeaderboard => true;
|
||||
|
||||
public ReplayPlayer(Score score, PlayerConfiguration configuration = null)
|
||||
: this((_, _) => score, configuration)
|
||||
{
|
||||
@@ -60,12 +63,6 @@ namespace osu.Game.Screens.Play
|
||||
this.createScore = createScore;
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private LeaderboardManager leaderboardManager { get; set; } = null!;
|
||||
|
||||
private readonly IBindable<LeaderboardScores> globalScores = new Bindable<LeaderboardScores>();
|
||||
private readonly BindableList<ScoreInfo> localScores = new BindableList<ScoreInfo>();
|
||||
|
||||
/// <summary>
|
||||
/// Add a settings group to the HUD overlay. Intended to be used by rulesets to add replay-specific settings.
|
||||
/// </summary>
|
||||
@@ -82,6 +79,8 @@ namespace osu.Game.Screens.Play
|
||||
if (!LoadedBeatmapSuccessfully)
|
||||
return;
|
||||
|
||||
AddInternal(leaderboardProvider);
|
||||
|
||||
playbackSettings = new PlaybackSettings
|
||||
{
|
||||
Depth = float.MaxValue,
|
||||
@@ -94,20 +93,6 @@ namespace osu.Game.Screens.Play
|
||||
HUDOverlay.PlayerSettingsOverlay.AddAtStart(playbackSettings);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
globalScores.BindTo(leaderboardManager.Scores);
|
||||
globalScores.BindValueChanged(_ =>
|
||||
{
|
||||
localScores.Clear();
|
||||
|
||||
if (globalScores.Value is LeaderboardScores g)
|
||||
localScores.AddRange(g.AllScores.OrderByTotalScore());
|
||||
}, true);
|
||||
}
|
||||
|
||||
protected override void PrepareReplay()
|
||||
{
|
||||
DrawableRuleset?.SetReplayScore(Score);
|
||||
@@ -118,13 +103,6 @@ namespace osu.Game.Screens.Play
|
||||
// Don't re-import replay scores as they're already present in the database.
|
||||
protected override Task ImportScore(Score score) => Task.CompletedTask;
|
||||
|
||||
protected override GameplayLeaderboard CreateGameplayLeaderboard() =>
|
||||
new SoloGameplayLeaderboard(Score.ScoreInfo.User)
|
||||
{
|
||||
AlwaysVisible = { Value = true },
|
||||
Scores = { BindTarget = localScores }
|
||||
};
|
||||
|
||||
protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score)
|
||||
{
|
||||
// Only show the relevant button otherwise things look silly.
|
||||
|
||||
@@ -5,50 +5,34 @@
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Online.Solo;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
{
|
||||
public partial class SoloPlayer : SubmittingPlayer
|
||||
{
|
||||
public SoloPlayer()
|
||||
: this(null)
|
||||
{
|
||||
}
|
||||
protected override bool ShowLeaderboard => true;
|
||||
|
||||
protected SoloPlayer(PlayerConfiguration configuration = null)
|
||||
[Cached(typeof(IGameplayLeaderboardProvider))]
|
||||
private readonly SoloGameplayLeaderboardProvider leaderboardProvider = new SoloGameplayLeaderboardProvider();
|
||||
|
||||
public SoloPlayer([CanBeNull] PlayerConfiguration configuration = null)
|
||||
: base(configuration)
|
||||
{
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private LeaderboardManager leaderboardManager { get; set; } = null!;
|
||||
|
||||
private readonly IBindable<LeaderboardScores> globalScores = new Bindable<LeaderboardScores>();
|
||||
private readonly BindableList<ScoreInfo> localScores = new BindableList<ScoreInfo>();
|
||||
|
||||
protected override void LoadComplete()
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
globalScores.BindTo(leaderboardManager.Scores);
|
||||
globalScores.BindValueChanged(_ =>
|
||||
{
|
||||
localScores.Clear();
|
||||
|
||||
if (globalScores.Value is LeaderboardScores g)
|
||||
localScores.AddRange(g.AllScores.OrderByTotalScore());
|
||||
}, true);
|
||||
AddInternal(leaderboardProvider);
|
||||
}
|
||||
|
||||
protected override APIRequest<APIScoreToken> CreateTokenRequest()
|
||||
@@ -65,30 +49,13 @@ namespace osu.Game.Screens.Play
|
||||
return new CreateSoloScoreRequest(Beatmap.Value.BeatmapInfo, rulesetId, Game.VersionHash);
|
||||
}
|
||||
|
||||
protected override GameplayLeaderboard CreateGameplayLeaderboard() =>
|
||||
new SoloGameplayLeaderboard(Score.ScoreInfo.User)
|
||||
{
|
||||
AlwaysVisible = { Value = false },
|
||||
Scores = { BindTarget = localScores }
|
||||
};
|
||||
|
||||
protected override bool ShouldExitOnTokenRetrievalFailure(Exception exception) => false;
|
||||
|
||||
protected override Task ImportScore(Score score)
|
||||
{
|
||||
// Before importing a score, stop binding the leaderboard with its score source.
|
||||
// This avoids a case where the imported score may cause a leaderboard refresh
|
||||
// (if the leaderboard's source is local).
|
||||
globalScores.UnbindBindings();
|
||||
|
||||
return base.ImportScore(score);
|
||||
}
|
||||
|
||||
protected override APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, long token)
|
||||
{
|
||||
IBeatmapInfo beatmap = score.ScoreInfo.BeatmapInfo;
|
||||
IBeatmapInfo beatmap = score.ScoreInfo.BeatmapInfo!;
|
||||
|
||||
Debug.Assert(beatmap!.OnlineID > 0);
|
||||
Debug.Assert(beatmap.OnlineID > 0);
|
||||
|
||||
return new SubmitSoloScoreRequest(score.ScoreInfo, token, beatmap.OnlineID);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Rulesets;
|
||||
@@ -71,9 +70,6 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
[Resolved]
|
||||
private IBindable<IReadOnlyList<Mod>> mods { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private LeaderboardManager leaderboardManager { get; set; } = null!;
|
||||
|
||||
@@ -94,44 +90,7 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
protected override APIRequest? FetchScores(CancellationToken cancellationToken)
|
||||
{
|
||||
var fetchBeatmapInfo = BeatmapInfo;
|
||||
|
||||
if (fetchBeatmapInfo == null)
|
||||
{
|
||||
SetErrorState(LeaderboardState.NoneSelected);
|
||||
return null;
|
||||
}
|
||||
|
||||
var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset;
|
||||
|
||||
if (!api.IsLoggedIn && IsOnlineScope)
|
||||
{
|
||||
SetErrorState(LeaderboardState.NotLoggedIn);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!fetchRuleset.IsLegacyRuleset())
|
||||
{
|
||||
SetErrorState(LeaderboardState.RulesetUnavailable);
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((fetchBeatmapInfo.OnlineID <= 0 || fetchBeatmapInfo.Status <= BeatmapOnlineStatus.Pending) && IsOnlineScope)
|
||||
{
|
||||
SetErrorState(LeaderboardState.BeatmapUnavailable);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Scope.RequiresSupporter(filterMods) && !api.LocalUser.Value.IsSupporter)
|
||||
{
|
||||
SetErrorState(LeaderboardState.NotSupporter);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Scope == BeatmapLeaderboardScope.Team && api.LocalUser.Value.Team == null)
|
||||
{
|
||||
SetErrorState(LeaderboardState.NoTeam);
|
||||
return null;
|
||||
}
|
||||
var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo?.Ruleset;
|
||||
|
||||
leaderboardManager.FetchWithCriteriaAsync(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope, filterMods ? mods.Value.ToArray() : null))
|
||||
.ContinueWith(t =>
|
||||
@@ -145,8 +104,12 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
fetchedScores.UnbindEvents();
|
||||
fetchedScores.BindValueChanged(scores =>
|
||||
{
|
||||
if (scores.NewValue != null)
|
||||
if (scores.NewValue == null) return;
|
||||
|
||||
if (scores.NewValue.FailState == null)
|
||||
Schedule(() => SetScores(scores.NewValue.TopScores, scores.NewValue.UserScore));
|
||||
else
|
||||
Schedule(() => SetErrorState((LeaderboardState)scores.NewValue.FailState));
|
||||
}, true);
|
||||
}, cancellationToken);
|
||||
|
||||
|
||||
@@ -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 System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Online.Spectator;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Screens.Select.Leaderboards
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a score shown on a gameplay leaderboard.
|
||||
/// The score is expected to update itself as gameplay progresses.
|
||||
/// </summary>
|
||||
public class GameplayLeaderboardScore
|
||||
{
|
||||
/// <summary>
|
||||
/// The user playing.
|
||||
/// </summary>
|
||||
public IUser User { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the score is being tracked.
|
||||
/// Generally understood as true when this score is the score of the local user currently playing.
|
||||
/// </summary>
|
||||
public bool Tracked { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The current total of the score.
|
||||
/// </summary>
|
||||
public BindableLong TotalScore { get; } = new BindableLong();
|
||||
|
||||
/// <summary>
|
||||
/// The current accuracy of the score.
|
||||
/// </summary>
|
||||
public BindableDouble Accuracy { get; } = new BindableDouble();
|
||||
|
||||
/// <summary>
|
||||
/// The current combo of the score.
|
||||
/// </summary>
|
||||
public BindableInt Combo { get; } = new BindableInt();
|
||||
|
||||
/// <summary>
|
||||
/// Whether the user playing has quit.
|
||||
/// </summary>
|
||||
public BindableBool HasQuit { get; } = new BindableBool();
|
||||
|
||||
/// <summary>
|
||||
/// An optional value to guarantee stable ordering.
|
||||
/// Lower numbers will appear higher in cases of <see cref="TotalScore"/> ties.
|
||||
/// </summary>
|
||||
public Bindable<long> DisplayOrder { get; } = new BindableLong();
|
||||
|
||||
/// <summary>
|
||||
/// A custom function which handles converting a score to a display score using a provided <see cref="ScoringMode"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If no function is provided, <see cref="TotalScore"/> will be used verbatim.
|
||||
/// </remarks>
|
||||
public Func<ScoringMode, long> GetDisplayScore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The colour of the team that the user playing is on, if any.
|
||||
/// </summary>
|
||||
public Colour4? TeamColour { get; init; }
|
||||
|
||||
public GameplayLeaderboardScore(IUser user, ScoreProcessor scoreProcessor, bool tracked)
|
||||
{
|
||||
User = user;
|
||||
Tracked = tracked;
|
||||
TotalScore.BindTarget = scoreProcessor.TotalScore;
|
||||
Accuracy.BindTarget = scoreProcessor.Accuracy;
|
||||
Combo.BindTarget = scoreProcessor.Combo;
|
||||
GetDisplayScore = scoreProcessor.GetDisplayScore;
|
||||
}
|
||||
|
||||
public GameplayLeaderboardScore(IUser user, SpectatorScoreProcessor scoreProcessor, bool tracked)
|
||||
{
|
||||
User = user;
|
||||
Tracked = tracked;
|
||||
TotalScore.BindTarget = scoreProcessor.TotalScore;
|
||||
Accuracy.BindTarget = scoreProcessor.Accuracy;
|
||||
Combo.BindTarget = scoreProcessor.Combo;
|
||||
GetDisplayScore = scoreProcessor.GetDisplayScore;
|
||||
}
|
||||
|
||||
public GameplayLeaderboardScore(ScoreInfo scoreInfo, bool tracked)
|
||||
{
|
||||
User = scoreInfo.User;
|
||||
Tracked = tracked;
|
||||
TotalScore.Value = scoreInfo.TotalScore;
|
||||
Accuracy.Value = scoreInfo.Accuracy;
|
||||
Combo.Value = scoreInfo.Combo;
|
||||
DisplayOrder.Value = scoreInfo.OnlineID > 0 ? scoreInfo.OnlineID : scoreInfo.Date.ToUnixTimeSeconds();
|
||||
GetDisplayScore = scoreInfo.GetDisplayScore;
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Used for testing.
|
||||
/// </remarks>
|
||||
internal GameplayLeaderboardScore(IUser user, bool tracked, Bindable<long> displayScore)
|
||||
{
|
||||
User = user;
|
||||
Tracked = tracked;
|
||||
TotalScore.BindTarget = displayScore;
|
||||
GetDisplayScore = _ => displayScore.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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.Bindables;
|
||||
|
||||
namespace osu.Game.Screens.Select.Leaderboards
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a leaderboard to show during gameplay.
|
||||
/// </summary>
|
||||
public interface IGameplayLeaderboardProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// List of all scores to display on the leaderboard.
|
||||
/// </summary>
|
||||
public IBindableList<GameplayLeaderboardScore> Scores { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this leaderboard is a partial leaderboard (e.g. contains only the top 50 of all scores),
|
||||
/// or is a full leaderboard (contains all scores that there will ever be).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If this is <see langword="true"/> and a tracked score is last on the leaderboard, it will show an "unknown" score position.
|
||||
/// </remarks>
|
||||
bool IsPartial { get; }
|
||||
}
|
||||
}
|
||||
+3
-4
@@ -4,13 +4,12 @@
|
||||
using System;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
namespace osu.Game.Screens.Select.Leaderboards
|
||||
{
|
||||
public partial class MultiSpectatorLeaderboard : MultiplayerGameplayLeaderboard
|
||||
public partial class MultiSpectatorLeaderboardProvider : MultiplayerLeaderboardProvider
|
||||
{
|
||||
public MultiSpectatorLeaderboard(MultiplayerRoomUser[] users)
|
||||
public MultiSpectatorLeaderboardProvider(MultiplayerRoomUser[] users)
|
||||
: base(users)
|
||||
{
|
||||
}
|
||||
+48
-65
@@ -11,6 +11,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
@@ -20,20 +21,30 @@ using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
|
||||
using osu.Game.Online.Spectator;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Users;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
namespace osu.Game.Screens.Select.Leaderboards
|
||||
{
|
||||
[LongRunningLoad]
|
||||
public partial class MultiplayerGameplayLeaderboard : GameplayLeaderboard
|
||||
public partial class MultiplayerLeaderboardProvider : CompositeComponent, IGameplayLeaderboardProvider
|
||||
{
|
||||
protected readonly Dictionary<int, TrackedUserData> UserScores = new Dictionary<int, TrackedUserData>();
|
||||
public IBindableList<GameplayLeaderboardScore> Scores => scores;
|
||||
private readonly BindableList<GameplayLeaderboardScore> scores = new BindableList<GameplayLeaderboardScore>();
|
||||
|
||||
protected readonly Dictionary<int, TrackedUserData> UserScores = new Dictionary<int, TrackedUserData>();
|
||||
public readonly SortedDictionary<int, BindableLong> TeamScores = new SortedDictionary<int, BindableLong>();
|
||||
|
||||
public bool HasTeams => TeamScores.Count > 0;
|
||||
|
||||
public bool IsPartial => false;
|
||||
|
||||
private readonly MultiplayerRoomUser[] users;
|
||||
|
||||
private readonly Bindable<ScoringMode> scoringMode = new Bindable<ScoringMode>();
|
||||
private readonly IBindableList<int> playingUserIds = new BindableList<int>();
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
private UserLookupCache userLookupCache { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private SpectatorClient spectatorClient { get; set; } = null!;
|
||||
@@ -42,31 +53,19 @@ namespace osu.Game.Screens.Play.HUD
|
||||
private MultiplayerClient multiplayerClient { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private UserLookupCache userLookupCache { get; set; } = null!;
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
private Bindable<ScoringMode> scoringMode = null!;
|
||||
|
||||
private readonly MultiplayerRoomUser[] playingUsers;
|
||||
|
||||
private readonly IBindableList<int> playingUserIds = new BindableList<int>();
|
||||
|
||||
private bool hasTeams => TeamScores.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a new leaderboard.
|
||||
/// </summary>
|
||||
/// <param name="users">IDs of all users in this match.</param>
|
||||
public MultiplayerGameplayLeaderboard(MultiplayerRoomUser[] users)
|
||||
public MultiplayerLeaderboardProvider(MultiplayerRoomUser[] users)
|
||||
{
|
||||
playingUsers = users;
|
||||
this.users = users;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config, IAPIProvider api, CancellationToken cancellationToken)
|
||||
{
|
||||
scoringMode = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode);
|
||||
config.BindWith(OsuSetting.ScoreDisplayMode, scoringMode);
|
||||
|
||||
foreach (var user in playingUsers)
|
||||
foreach (var user in users)
|
||||
{
|
||||
var scoreProcessor = new SpectatorScoreProcessor(user.UserID);
|
||||
scoreProcessor.Mode.BindTo(scoringMode);
|
||||
@@ -80,29 +79,29 @@ namespace osu.Game.Screens.Play.HUD
|
||||
TeamScores.Add(team, new BindableLong());
|
||||
}
|
||||
|
||||
userLookupCache.GetUsersAsync(playingUsers.Select(u => u.UserID).ToArray(), cancellationToken)
|
||||
userLookupCache.GetUsersAsync(users.Select(u => u.UserID).ToArray(), cancellationToken)
|
||||
.ContinueWith(task =>
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
var users = task.GetResultSafely();
|
||||
var lookedUpUsers = task.GetResultSafely();
|
||||
|
||||
for (int i = 0; i < users.Length; i++)
|
||||
for (int i = 0; i < lookedUpUsers.Length; i++)
|
||||
{
|
||||
var user = users[i] ?? new APIUser
|
||||
var user = lookedUpUsers[i] ?? new APIUser
|
||||
{
|
||||
Id = playingUsers[i].UserID,
|
||||
Id = users[i].UserID,
|
||||
Username = "Unknown user",
|
||||
};
|
||||
|
||||
var trackedUser = UserScores[user.Id];
|
||||
|
||||
var leaderboardScore = Add(user, user.Id == api.LocalUser.Value.Id);
|
||||
leaderboardScore.GetDisplayScore = trackedUser.ScoreProcessor.GetDisplayScore;
|
||||
leaderboardScore.Accuracy.BindTo(trackedUser.ScoreProcessor.Accuracy);
|
||||
leaderboardScore.TotalScore.BindTo(trackedUser.ScoreProcessor.TotalScore);
|
||||
leaderboardScore.Combo.BindTo(trackedUser.ScoreProcessor.Combo);
|
||||
leaderboardScore.HasQuit.BindTo(trackedUser.UserQuit);
|
||||
var leaderboardScore = new GameplayLeaderboardScore(user, trackedUser.ScoreProcessor, user.Id == api.LocalUser.Value.Id)
|
||||
{
|
||||
HasQuit = { BindTarget = trackedUser.UserQuit },
|
||||
TeamColour = UserScores[user.OnlineID].Team is int team ? getTeamColour(team) : null,
|
||||
};
|
||||
scores.Add(leaderboardScore);
|
||||
}
|
||||
});
|
||||
}, cancellationToken);
|
||||
@@ -113,7 +112,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
base.LoadComplete();
|
||||
|
||||
// BindableList handles binding in a really bad way (Clear then AddRange) so we need to do this manually..
|
||||
foreach (var user in playingUsers)
|
||||
foreach (var user in users)
|
||||
{
|
||||
spectatorClient.WatchUser(user.UserID);
|
||||
|
||||
@@ -127,34 +126,6 @@ namespace osu.Game.Screens.Play.HUD
|
||||
playingUserIds.BindCollectionChanged(playingUsersChanged);
|
||||
}
|
||||
|
||||
protected override GameplayLeaderboardScore CreateLeaderboardScoreDrawable(IUser? user, bool isTracked)
|
||||
{
|
||||
var leaderboardScore = base.CreateLeaderboardScoreDrawable(user, isTracked);
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
if (UserScores[user.OnlineID].Team is int team)
|
||||
{
|
||||
leaderboardScore.BackgroundColour = getTeamColour(team).Lighten(1.2f);
|
||||
leaderboardScore.TextColour = Color4.White;
|
||||
}
|
||||
}
|
||||
|
||||
return leaderboardScore;
|
||||
}
|
||||
|
||||
private Color4 getTeamColour(int team)
|
||||
{
|
||||
switch (team)
|
||||
{
|
||||
case 0:
|
||||
return colours.TeamColourRed;
|
||||
|
||||
default:
|
||||
return colours.TeamColourBlue;
|
||||
}
|
||||
}
|
||||
|
||||
private void playingUsersChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
switch (e.Action)
|
||||
@@ -176,10 +147,10 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
private void updateTotals()
|
||||
{
|
||||
if (!hasTeams)
|
||||
if (!HasTeams)
|
||||
return;
|
||||
|
||||
foreach (var scores in TeamScores.Values) scores.Value = 0;
|
||||
foreach (var teamTotal in TeamScores.Values) teamTotal.Value = 0;
|
||||
|
||||
foreach (var u in UserScores.Values)
|
||||
{
|
||||
@@ -191,13 +162,25 @@ namespace osu.Game.Screens.Play.HUD
|
||||
}
|
||||
}
|
||||
|
||||
private Color4 getTeamColour(int team)
|
||||
{
|
||||
switch (team)
|
||||
{
|
||||
case 0:
|
||||
return colours.TeamColourRed.Lighten(1.2f);
|
||||
|
||||
default:
|
||||
return colours.TeamColourBlue.Lighten(1.2f);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (spectatorClient.IsNotNull())
|
||||
{
|
||||
foreach (var user in playingUsers)
|
||||
foreach (var user in users)
|
||||
spectatorClient.StopWatchingUser(user.UserID);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
namespace osu.Game.Screens.Select.Leaderboards
|
||||
{
|
||||
public partial class SoloGameplayLeaderboardProvider : Component, IGameplayLeaderboardProvider
|
||||
{
|
||||
public bool IsPartial { get; private set; }
|
||||
|
||||
public IBindableList<GameplayLeaderboardScore> Scores => scores;
|
||||
private readonly BindableList<GameplayLeaderboardScore> scores = new BindableList<GameplayLeaderboardScore>();
|
||||
|
||||
[Resolved]
|
||||
private LeaderboardManager? leaderboardManager { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private GameplayState? gameplayState { get; set; }
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
var globalScores = leaderboardManager?.Scores.Value;
|
||||
|
||||
IsPartial = leaderboardManager?.CurrentCriteria?.Scope != BeatmapLeaderboardScope.Local && globalScores?.TopScores.Count >= 50;
|
||||
|
||||
if (globalScores != null)
|
||||
{
|
||||
foreach (var topScore in globalScores.AllScores.OrderByTotalScore())
|
||||
scores.Add(new GameplayLeaderboardScore(topScore, false));
|
||||
}
|
||||
|
||||
if (gameplayState != null)
|
||||
{
|
||||
scores.Add(new GameplayLeaderboardScore(gameplayState.Score.ScoreInfo.User, gameplayState.ScoreProcessor, true)
|
||||
{
|
||||
// Local score should always show lower than any existing scores in cases of ties.
|
||||
DisplayOrder = { Value = long.MaxValue }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,9 @@ namespace osu.Game.Screens.SelectV2
|
||||
var newItems = new List<CarouselItem>();
|
||||
|
||||
BeatmapInfo? lastBeatmap = null;
|
||||
|
||||
GroupDefinition? lastGroup = null;
|
||||
CarouselItem? lastGroupItem = null;
|
||||
|
||||
HashSet<CarouselItem>? currentGroupItems = null;
|
||||
HashSet<CarouselItem>? currentSetItems = null;
|
||||
@@ -69,7 +71,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
groupItems[newGroup] = currentGroupItems = new HashSet<CarouselItem>();
|
||||
lastGroup = newGroup;
|
||||
|
||||
addItem(new CarouselItem(newGroup)
|
||||
addItem(lastGroupItem = new CarouselItem(newGroup)
|
||||
{
|
||||
DrawHeight = PanelGroup.HEIGHT,
|
||||
DepthLayer = -2,
|
||||
@@ -84,6 +86,9 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
setItems[beatmap.BeatmapSet!] = currentSetItems = new HashSet<CarouselItem>();
|
||||
|
||||
if (lastGroupItem != null)
|
||||
lastGroupItem.NestedItemCount++;
|
||||
|
||||
addItem(new CarouselItem(beatmap.BeatmapSet!)
|
||||
{
|
||||
DrawHeight = PanelBeatmapSet.HEIGHT,
|
||||
@@ -91,6 +96,11 @@ namespace osu.Game.Screens.SelectV2
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (lastGroupItem != null)
|
||||
lastGroupItem.NestedItemCount++;
|
||||
}
|
||||
|
||||
addItem(item);
|
||||
lastBeatmap = beatmap;
|
||||
|
||||
@@ -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.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osuTK;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
/// <summary>
|
||||
/// A dropdown to select the collection to be used to filter results.
|
||||
/// </summary>
|
||||
public partial class CollectionDropdown : ShearedDropdown<CollectionFilterMenuItem> // TODO: partial class under FilterControl?
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether to show the "manage collections..." menu item in the dropdown.
|
||||
/// </summary>
|
||||
protected virtual bool ShowManageCollectionsItem => true;
|
||||
|
||||
public Action? RequestFilter { private get; set; }
|
||||
|
||||
private readonly BindableList<CollectionFilterMenuItem> filters = new BindableList<CollectionFilterMenuItem>();
|
||||
|
||||
[Resolved]
|
||||
private ManageCollectionsDialog? manageCollectionsDialog { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
|
||||
private IDisposable? realmSubscription;
|
||||
|
||||
private readonly CollectionFilterMenuItem allBeatmapsItem = new AllBeatmapsCollectionFilterMenuItem();
|
||||
|
||||
public CollectionDropdown()
|
||||
: base("Collection")
|
||||
{
|
||||
ItemSource = filters;
|
||||
|
||||
Current.Value = allBeatmapsItem;
|
||||
AlwaysShowSearchBar = true;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
realmSubscription = realm.RegisterForNotifications(r => r.All<BeatmapCollection>().OrderBy(c => c.Name), collectionsChanged);
|
||||
|
||||
Current.BindValueChanged(selectionChanged);
|
||||
}
|
||||
|
||||
private void collectionsChanged(IRealmCollection<BeatmapCollection> collections, ChangeSet? changes)
|
||||
{
|
||||
if (changes == null)
|
||||
{
|
||||
filters.Clear();
|
||||
filters.Add(allBeatmapsItem);
|
||||
filters.AddRange(collections.Select(c => new CollectionFilterMenuItem(c.ToLive(realm))));
|
||||
if (ShowManageCollectionsItem)
|
||||
filters.Add(new ManageCollectionsFilterMenuItem());
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (int i in changes.DeletedIndices.OrderDescending())
|
||||
filters.RemoveAt(i + 1);
|
||||
|
||||
foreach (int i in changes.InsertedIndices)
|
||||
filters.Insert(i + 1, new CollectionFilterMenuItem(collections[i].ToLive(realm)));
|
||||
|
||||
var selectedItem = SelectedItem?.Value;
|
||||
|
||||
foreach (int i in changes.NewModifiedIndices)
|
||||
{
|
||||
var updatedItem = collections[i];
|
||||
|
||||
// This is responsible for updating the state of the +/- button and the collection's name.
|
||||
// TODO: we can probably make the menu items update with changes to avoid this.
|
||||
filters.RemoveAt(i + 1);
|
||||
filters.Insert(i + 1, new CollectionFilterMenuItem(updatedItem.ToLive(realm)));
|
||||
|
||||
if (updatedItem.ID == selectedItem?.Collection?.ID)
|
||||
{
|
||||
// This current update and schedule is required to work around dropdown headers not updating text even when the selected item
|
||||
// changes. It's not great but honestly the whole dropdown menu structure isn't great. This needs to be fixed, but I'll issue
|
||||
// a warning that it's going to be a frustrating journey.
|
||||
Current.Value = allBeatmapsItem;
|
||||
Schedule(() =>
|
||||
{
|
||||
// current may have changed before the scheduled call is run.
|
||||
if (Current.Value != allBeatmapsItem)
|
||||
return;
|
||||
|
||||
Current.Value = filters.SingleOrDefault(f => f.Collection?.ID == selectedItem.Collection?.ID) ?? filters[0];
|
||||
});
|
||||
|
||||
// Trigger an external re-filter if the current item was in the change set.
|
||||
RequestFilter?.Invoke();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Live<BeatmapCollection>? lastFiltered;
|
||||
|
||||
private void selectionChanged(ValueChangedEvent<CollectionFilterMenuItem> filter)
|
||||
{
|
||||
// May be null during .Clear().
|
||||
if (filter.NewValue.IsNull())
|
||||
return;
|
||||
|
||||
// Never select the manage collection filter - rollback to the previous filter.
|
||||
// This is done after the above since it is important that bindable is unbound from OldValue, which is lost after forcing it back to the old value.
|
||||
if (filter.NewValue is ManageCollectionsFilterMenuItem)
|
||||
{
|
||||
Current.Value = filter.OldValue;
|
||||
manageCollectionsDialog?.Show();
|
||||
return;
|
||||
}
|
||||
|
||||
var newCollection = filter.NewValue.Collection;
|
||||
|
||||
// This dropdown be weird.
|
||||
// We only care about filtering if the actual collection has changed.
|
||||
if (newCollection != lastFiltered)
|
||||
{
|
||||
RequestFilter?.Invoke();
|
||||
lastFiltered = newCollection;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
realmSubscription?.Dispose();
|
||||
}
|
||||
|
||||
protected override LocalisableString GenerateItemText(CollectionFilterMenuItem item) => item.CollectionName;
|
||||
|
||||
protected sealed override DropdownMenu CreateMenu() => CreateCollectionMenu();
|
||||
|
||||
protected virtual ShearedCollectionDropdownMenu CreateCollectionMenu() => new ShearedCollectionDropdownMenu();
|
||||
|
||||
protected partial class ShearedCollectionDropdownMenu : ShearedDropdownMenu
|
||||
{
|
||||
public ShearedCollectionDropdownMenu()
|
||||
{
|
||||
MaxHeight = 200;
|
||||
}
|
||||
|
||||
protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new DrawableCollectionMenuItem(item)
|
||||
{
|
||||
BackgroundColourHover = HoverColour,
|
||||
BackgroundColourSelected = SelectionColour
|
||||
};
|
||||
}
|
||||
|
||||
protected partial class DrawableCollectionMenuItem : ShearedDropdownMenu.ShearedMenuItem
|
||||
{
|
||||
private IconButton addOrRemoveButton = null!;
|
||||
|
||||
private bool beatmapInCollection;
|
||||
|
||||
private readonly Live<BeatmapCollection>? collection;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
|
||||
|
||||
public DrawableCollectionMenuItem(MenuItem item)
|
||||
: base(item)
|
||||
{
|
||||
collection = ((DropdownMenuItem<CollectionFilterMenuItem>)item).Value.Collection;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AddInternal(addOrRemoveButton = new NoFocusChangeIconButton
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Shear = -OsuGame.SHEAR,
|
||||
X = -OsuScrollContainer.SCROLL_BAR_WIDTH,
|
||||
Scale = new Vector2(0.65f),
|
||||
Action = addOrRemove,
|
||||
});
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
if (collection != null)
|
||||
{
|
||||
beatmap.BindValueChanged(_ =>
|
||||
{
|
||||
beatmapInCollection = collection.PerformRead(c => c.BeatmapMD5Hashes.Contains(beatmap.Value.BeatmapInfo.MD5Hash));
|
||||
|
||||
addOrRemoveButton.Enabled.Value = !beatmap.IsDefault;
|
||||
addOrRemoveButton.Icon = beatmapInCollection ? FontAwesome.Solid.MinusSquare : FontAwesome.Solid.PlusSquare;
|
||||
addOrRemoveButton.TooltipText = beatmapInCollection ? "Remove selected beatmap" : "Add selected beatmap";
|
||||
|
||||
updateButtonVisibility();
|
||||
}, true);
|
||||
}
|
||||
|
||||
updateButtonVisibility();
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateButtonVisibility();
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
updateButtonVisibility();
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
|
||||
protected override void OnSelectChange()
|
||||
{
|
||||
base.OnSelectChange();
|
||||
updateButtonVisibility();
|
||||
}
|
||||
|
||||
private void updateButtonVisibility()
|
||||
{
|
||||
if (collection == null)
|
||||
addOrRemoveButton.Alpha = 0;
|
||||
else
|
||||
addOrRemoveButton.Alpha = IsHovered || IsPreSelected || beatmapInCollection ? 1 : 0;
|
||||
}
|
||||
|
||||
private void addOrRemove()
|
||||
{
|
||||
Debug.Assert(collection != null);
|
||||
|
||||
collection.PerformWrite(c =>
|
||||
{
|
||||
if (!c.BeatmapMD5Hashes.Remove(beatmap.Value.BeatmapInfo.MD5Hash))
|
||||
c.BeatmapMD5Hashes.Add(beatmap.Value.BeatmapInfo.MD5Hash);
|
||||
});
|
||||
}
|
||||
|
||||
protected override Drawable CreateContent() => (Content)base.CreateContent();
|
||||
|
||||
private partial class NoFocusChangeIconButton : IconButton
|
||||
{
|
||||
public override bool ChangeFocusOnClick => false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
Icon = difficultyIcon = new ConstrainedIconContainer
|
||||
{
|
||||
Size = new Vector2(20),
|
||||
Size = new Vector2(16f),
|
||||
Margin = new MarginPadding { Horizontal = 5f },
|
||||
Colour = colourProvider.Background5,
|
||||
};
|
||||
@@ -100,12 +100,13 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Scale = new Vector2(0.875f),
|
||||
},
|
||||
localRank = new PanelLocalRankDisplay
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Scale = new Vector2(0.75f)
|
||||
Scale = new Vector2(0.65f)
|
||||
},
|
||||
starCounter = new StarCounter
|
||||
{
|
||||
@@ -123,22 +124,22 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
keyCountText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold),
|
||||
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Alpha = 0,
|
||||
},
|
||||
difficultyText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold),
|
||||
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Margin = new MarginPadding { Right = 8f },
|
||||
Margin = new MarginPadding { Right = 5f },
|
||||
},
|
||||
authorText = new OsuSpriteText
|
||||
{
|
||||
Colour = colourProvider.Content2,
|
||||
Font = OsuFont.GetFont(weight: FontWeight.SemiBold),
|
||||
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class PanelBeatmapSet : Panel
|
||||
{
|
||||
public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f;
|
||||
public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.7f;
|
||||
|
||||
private PanelSetBackground background = null!;
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Icon = FontAwesome.Solid.ChevronRight,
|
||||
Size = new Vector2(12),
|
||||
Size = new Vector2(8),
|
||||
X = 1f,
|
||||
Colour = colourProvider.Background5,
|
||||
},
|
||||
@@ -77,17 +77,17 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
titleText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true),
|
||||
Font = OsuFont.Style.Heading1.With(typeface: Typeface.TorusAlternate),
|
||||
},
|
||||
artistText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true),
|
||||
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Direction = FillDirection.Horizontal,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Margin = new MarginPadding { Top = 5f },
|
||||
Margin = new MarginPadding { Top = 4f },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
updateButton = new PanelUpdateBeatmapButton
|
||||
@@ -100,8 +100,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
TextSize = 11,
|
||||
TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 },
|
||||
TextSize = OsuFont.Style.Caption2.Size,
|
||||
Margin = new MarginPadding { Right = 5f },
|
||||
},
|
||||
difficultiesDisplay = new DifficultySpectrumDisplay
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class PanelBeatmapStandalone : Panel
|
||||
{
|
||||
public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f;
|
||||
public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.7f;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
|
||||
@@ -76,7 +76,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
Icon = difficultyIcon = new ConstrainedIconContainer
|
||||
{
|
||||
Size = new Vector2(20),
|
||||
Size = new Vector2(16),
|
||||
Margin = new MarginPadding { Horizontal = 5f },
|
||||
Colour = colourProvider.Background5,
|
||||
};
|
||||
@@ -95,19 +95,16 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
titleText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true),
|
||||
Shadow = true,
|
||||
Font = OsuFont.Style.Heading1.With(typeface: Typeface.TorusAlternate),
|
||||
},
|
||||
artistText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true),
|
||||
Shadow = true,
|
||||
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Direction = FillDirection.Horizontal,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Margin = new MarginPadding { Top = 5f },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
updateButton = new PanelUpdateBeatmapButton
|
||||
@@ -120,8 +117,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
TextSize = 11,
|
||||
TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 },
|
||||
TextSize = OsuFont.Style.Caption2.Size,
|
||||
Margin = new MarginPadding { Right = 5f },
|
||||
},
|
||||
difficultyLine = new FillFlowContainer
|
||||
@@ -134,19 +130,19 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Scale = new Vector2(8f / 9f),
|
||||
Scale = new Vector2(0.875f),
|
||||
Margin = new MarginPadding { Right = 5f },
|
||||
},
|
||||
difficultyRank = new PanelLocalRankDisplay
|
||||
{
|
||||
Scale = new Vector2(8f / 11),
|
||||
Scale = new Vector2(0.65f),
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Margin = new MarginPadding { Right = 5f },
|
||||
},
|
||||
difficultyKeyCountText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold),
|
||||
Font = OsuFont.Style.Heading2,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Alpha = 0,
|
||||
@@ -154,7 +150,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
},
|
||||
difficultyName = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold),
|
||||
Font = OsuFont.Style.Heading2,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Margin = new MarginPadding { Right = 5f, Bottom = 2f },
|
||||
@@ -162,7 +158,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
difficultyAuthor = new OsuSpriteText
|
||||
{
|
||||
Colour = colourProvider.Content2,
|
||||
Font = OsuFont.GetFont(weight: FontWeight.SemiBold),
|
||||
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
|
||||
Origin = Anchor.BottomLeft,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Margin = new MarginPadding { Right = 5f, Bottom = 2f },
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
private Drawable iconContainer = null!;
|
||||
private OsuSpriteText titleText = null!;
|
||||
private TrianglesV2 triangles = null!;
|
||||
private OsuSpriteText countText = null!;
|
||||
private Box glow = null!;
|
||||
|
||||
[Resolved]
|
||||
@@ -99,13 +100,11 @@ namespace osu.Game.Screens.SelectV2
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Black.Opacity(0.7f),
|
||||
},
|
||||
new OsuSpriteText
|
||||
countText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold),
|
||||
// TODO: requires Carousel/CarouselItem-side implementation
|
||||
Text = "43",
|
||||
UseFullGlyphHeight = false,
|
||||
}
|
||||
},
|
||||
@@ -144,6 +143,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
GroupDefinition group = (GroupDefinition)Item.Model;
|
||||
|
||||
titleText.Text = group.Title;
|
||||
countText.Text = Item.NestedItemCount.ToString("N0");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
public PanelUpdateBeatmapButton()
|
||||
{
|
||||
Size = new Vector2(75f, 22f);
|
||||
Size = new Vector2(72, 22f);
|
||||
}
|
||||
|
||||
private Bindable<bool> preferNoVideo = null!;
|
||||
@@ -63,7 +63,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
const float icon_size = 14;
|
||||
const float icon_size = 12;
|
||||
|
||||
preferNoVideo = config.GetBindable<bool>(OsuSetting.PreferNoVideo);
|
||||
|
||||
@@ -110,7 +110,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Font = OsuFont.Default.With(weight: FontWeight.Bold),
|
||||
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
|
||||
Text = "Update",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace osu.Game.Storyboards
|
||||
private readonly List<StoryboardTriggerGroup> triggerGroups = new List<StoryboardTriggerGroup>();
|
||||
|
||||
public string Path { get; }
|
||||
public bool IsDrawable => HasCommands;
|
||||
public virtual bool IsDrawable => HasCommands;
|
||||
|
||||
public Anchor Origin;
|
||||
public Vector2 InitialPosition;
|
||||
|
||||
@@ -19,6 +19,8 @@ namespace osu.Game.Storyboards
|
||||
|
||||
public override double StartTime { get; }
|
||||
|
||||
public override bool IsDrawable => true;
|
||||
|
||||
public override Drawable CreateDrawable() => new DrawableStoryboardVideo(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
// 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.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.IO.Legacy;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
namespace osu.Game.Tests.Visual
|
||||
{
|
||||
/// <summary>
|
||||
/// The goal of this abstract test class is to exercise correct playback of replays sourced from previous osu! versions.
|
||||
/// Use <see cref="RunTest"/> to exercise that property.
|
||||
/// </summary>
|
||||
[HeadlessTest]
|
||||
[TestFixture]
|
||||
public abstract partial class LegacyReplayPlaybackTestScene : RateAdjustedBeatmapTestScene
|
||||
{
|
||||
private ReplayPlayer currentPlayer = null!;
|
||||
private readonly List<JudgementResult> results = new List<JudgementResult>();
|
||||
|
||||
/// <summary>
|
||||
/// This is provided as a convenience for testing behaviour against osu!stable.
|
||||
/// Setting this field to a non-null path will cause beatmap files and replays used in all test cases
|
||||
/// to be exported to disk so that they can be cross-checked against stable.
|
||||
/// </summary>
|
||||
protected abstract string? ExportLocation { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Encodes the supplied <paramref name="originalScore"/>, decodes the result of encoding, runs the result of decoding against the supplied <paramref name="beatmap"/>,
|
||||
/// and checks that the judgement results recorded still match <paramref name="expectedResults"/>.
|
||||
/// If <see cref="ExportLocation"/> is set, exports both the beatmap and the replay to said location.
|
||||
/// </summary>
|
||||
protected void RunTest(string beatmapName, IBeatmap beatmap, string replayName, Score originalScore, IEnumerable<HitResult> expectedResults)
|
||||
{
|
||||
IBeatmap playableBeatmap = null!;
|
||||
MemoryStream beatmapStream = new MemoryStream();
|
||||
MemoryStream scoreStream = new MemoryStream();
|
||||
Score decodedScore = null!;
|
||||
|
||||
AddStep(@"set up beatmap", () =>
|
||||
{
|
||||
beatmap.Metadata.Title = beatmapName;
|
||||
Beatmap.Value = CreateWorkingBeatmap(beatmap);
|
||||
Ruleset.Value = CreateRuleset()!.RulesetInfo;
|
||||
playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
|
||||
|
||||
var beatmapEncoder = new LegacyBeatmapEncoder(beatmap, null);
|
||||
|
||||
using (var writer = new StreamWriter(beatmapStream, Encoding.UTF8, leaveOpen: true))
|
||||
beatmapEncoder.Encode(writer);
|
||||
|
||||
beatmapStream.Seek(0, SeekOrigin.Begin);
|
||||
playableBeatmap.BeatmapInfo.MD5Hash = beatmapStream.ComputeMD5Hash();
|
||||
});
|
||||
|
||||
AddStep(@"encode score", () =>
|
||||
{
|
||||
originalScore.ScoreInfo.BeatmapInfo = playableBeatmap.BeatmapInfo;
|
||||
var encoder = new LegacyScoreEncoder(originalScore, playableBeatmap);
|
||||
encoder.Encode(scoreStream, leaveOpen: true);
|
||||
|
||||
// `LegacyScoreEncoder` hardcodes a replay version that belongs to lazer.
|
||||
// here we want to simulate a stable replay, which should have the classic mod attached etc.
|
||||
// to that end, we do a post-encode step to specify a stable-like replay version.
|
||||
scoreStream.Position = 1;
|
||||
|
||||
using (var sw = new SerializationWriter(scoreStream, leaveOpen: true))
|
||||
{
|
||||
const int version = 20250414;
|
||||
sw.Write(version);
|
||||
}
|
||||
|
||||
scoreStream.Position = 0;
|
||||
});
|
||||
|
||||
if (ExportLocation != null)
|
||||
{
|
||||
AddStep("export beatmap", () =>
|
||||
{
|
||||
using var stream = File.Open(Path.Combine(ExportLocation, $"{beatmapName}.osu"), FileMode.Create);
|
||||
beatmapStream.CopyTo(stream);
|
||||
beatmapStream.Position = 0;
|
||||
});
|
||||
|
||||
AddStep("export score", () =>
|
||||
{
|
||||
using var stream = File.Open(Path.Combine(ExportLocation, $@"{replayName}.osr"), FileMode.Create);
|
||||
scoreStream.CopyTo(stream);
|
||||
scoreStream.Position = 0;
|
||||
});
|
||||
}
|
||||
|
||||
AddStep(@"decode score", () =>
|
||||
{
|
||||
using (scoreStream)
|
||||
{
|
||||
scoreStream.Position = 0;
|
||||
decodedScore = new TestScoreDecoder(Beatmap.Value, Ruleset.Value).Parse(scoreStream);
|
||||
}
|
||||
});
|
||||
|
||||
AddAssert(@"classic mod present", () => decodedScore.ScoreInfo.Mods.Any(mod => mod is ModClassic));
|
||||
AddStep(@"push player", () => pushNewPlayer(decodedScore));
|
||||
|
||||
AddUntilStep(@"Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
|
||||
AddAssert(@"classic mod present", () => currentPlayer.GameplayState.Mods.Any(mod => mod is ModClassic));
|
||||
AddUntilStep(@"Wait for completion", () => currentPlayer.GameplayState.HasCompleted);
|
||||
AddAssert(@"judgement results after encode are correct", () => results.Select(r => r.Type), () => Is.EquivalentTo(expectedResults));
|
||||
}
|
||||
|
||||
private void pushNewPlayer(Score score)
|
||||
{
|
||||
var player = new ReplayPlayer(score);
|
||||
SelectedMods.Value = score.ScoreInfo.Mods;
|
||||
player.OnLoadComplete += _ =>
|
||||
{
|
||||
player.GameplayState.ScoreProcessor.NewJudgement += result =>
|
||||
{
|
||||
if (currentPlayer == player)
|
||||
results.Add(result);
|
||||
};
|
||||
};
|
||||
LoadScreen(currentPlayer = player);
|
||||
results.Clear();
|
||||
}
|
||||
|
||||
private class TestScoreDecoder : LegacyScoreDecoder
|
||||
{
|
||||
private readonly WorkingBeatmap beatmap;
|
||||
private readonly Ruleset ruleset;
|
||||
|
||||
public TestScoreDecoder(WorkingBeatmap beatmap, RulesetInfo ruleset)
|
||||
{
|
||||
this.beatmap = beatmap;
|
||||
this.ruleset = ruleset.CreateInstance();
|
||||
}
|
||||
|
||||
protected override Ruleset GetRuleset(int rulesetId) => ruleset;
|
||||
protected override WorkingBeatmap GetBeatmap(string md5Hash) => beatmap;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="20.1.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2025.321.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2025.419.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2025.321.0" />
|
||||
<PackageReference Include="Sentry" Version="5.1.1" />
|
||||
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
|
||||
|
||||
+1
-1
@@ -17,6 +17,6 @@
|
||||
<MtouchInterpreter>-all</MtouchInterpreter>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2025.321.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2025.419.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user