From c231571f06167b4445148bf29ac70c4facb3f8e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 16 Apr 2025 13:46:35 +0200 Subject: [PATCH 01/31] Separate gameplay leaderboard data management from display This is a prerequisite for supporting skinning of leaderboards. - New `IGameplayLeaderboardProvider` and `IGameplayLeaderboardScore` interfaces are introduced. They are strictly concerned with supplying leaderboard data. - Logic of managing display, which was previously jammed into the inheritance hierarchy of `GameplayLeaderboard`, is now moved into `IGameplayLeaderboardProvider` implementations. Solo play, multiplayer, and multiplayer spectator get their own implementation of the interface. - The inheritance hierarchy of `GameplayLeaderboard` and per-player overriding of the implementation of the gameplay leaderboard is gone. Only one drawable class (renamed to `DrawableGameplayLeaderboard`) is allowed to display the leaderboards, across all modes of play. --- .../Gameplay/TestSceneGameplayLeaderboard.cs | 81 ++++++++++-- .../TestSceneSoloGameplayLeaderboard.cs | 124 ------------------ ...MultiplayerGameplayLeaderboardTestScene.cs | 39 +++++- .../TestSceneMultiSpectatorLeaderboard.cs | 31 +++-- .../TestSceneMultiSpectatorScreen.cs | 2 +- ...TestSceneMultiplayerGameplayLeaderboard.cs | 16 +-- ...ceneMultiplayerGameplayLeaderboardTeams.cs | 15 ++- .../Online/Leaderboards/LeaderboardManager.cs | 6 +- .../Multiplayer/MultiplayerPlayer.cs | 51 +++---- .../Spectate/MultiSpectatorScreen.cs | 28 ++-- ...oard.cs => DrawableGameplayLeaderboard.cs} | 59 +++++---- ...cs => DrawableGameplayLeaderboardScore.cs} | 26 ++-- .../Play/HUD/IGameplayLeaderboardScore.cs | 67 ++++++++++ .../Screens/Play/HUD/ILeaderboardScore.cs | 31 ----- .../Play/HUD/SoloGameplayLeaderboard.cs | 108 --------------- osu.Game/Screens/Play/Player.cs | 28 ++-- osu.Game/Screens/Play/ReplayPlayer.cs | 38 ++---- osu.Game/Screens/Play/SoloPlayer.cs | 57 ++------ .../Leaderboards/GameplayLeaderboardScore.cs | 59 +++++++++ .../IGameplayLeaderboardProvider.cs | 25 ++++ .../MultiSpectatorLeaderboardProvider.cs} | 7 +- .../MultiplayerLeaderboardProvider.cs} | 114 +++++++--------- .../SoloGameplayLeaderboardProvider.cs | 41 ++++++ 23 files changed, 508 insertions(+), 545 deletions(-) delete mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs rename osu.Game/Screens/Play/HUD/{GameplayLeaderboard.cs => DrawableGameplayLeaderboard.cs} (74%) rename osu.Game/Screens/Play/HUD/{GameplayLeaderboardScore.cs => DrawableGameplayLeaderboardScore.cs} (96%) create mode 100644 osu.Game/Screens/Play/HUD/IGameplayLeaderboardScore.cs delete mode 100644 osu.Game/Screens/Play/HUD/ILeaderboardScore.cs delete mode 100644 osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs create mode 100644 osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs create mode 100644 osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs rename osu.Game/Screens/{OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs => Select/Leaderboards/MultiSpectatorLeaderboardProvider.cs} (76%) rename osu.Game/Screens/{Play/HUD/MultiplayerGameplayLeaderboard.cs => Select/Leaderboards/MultiplayerLeaderboardProvider.cs} (68%) create mode 100644 osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index 1787230117..23cd262dd0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -1,11 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - +using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.PolygonExtensions; using osu.Framework.Graphics; @@ -15,7 +15,10 @@ using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Select.Leaderboards; +using osu.Game.Users; using osuTK; namespace osu.Game.Tests.Visual.Gameplay @@ -23,7 +26,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(); @@ -57,10 +63,10 @@ namespace osu.Game.Tests.Visual.Gameplay // has caused layout to not work in the past. AddUntilStep("wait for fill flow layout", - () => leaderboard.ChildrenOfType>().First().ScreenSpaceDrawQuad.Intersects(leaderboard.ScreenSpaceDrawQuad)); + () => leaderboard.ChildrenOfType>().First().ScreenSpaceDrawQuad.Intersects(leaderboard.ScreenSpaceDrawQuad)); AddUntilStep("wait for some scores not masked away", - () => leaderboard.ChildrenOfType().Any(s => leaderboard.ScreenSpaceDrawQuad.Contains(s.ScreenSpaceDrawQuad.Centre))); + () => leaderboard.ChildrenOfType().Any(s => leaderboard.ScreenSpaceDrawQuad.Contains(s.ScreenSpaceDrawQuad.Centre))); AddUntilStep("wait for tracked score fully visible", () => leaderboard.ScreenSpaceDrawQuad.Intersects(leaderboard.TrackedScore!.ScreenSpaceDrawQuad)); @@ -139,7 +145,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 +185,27 @@ 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)); + } + private void addLocalPlayer() { AddStep("add local player", () => @@ -188,11 +215,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 +234,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 TestDrawableGameplayLeaderboardScore(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 +249,36 @@ namespace osu.Game.Tests.Visual.Gameplay return scoreItem != null && scoreItem.ScorePosition == expectedPosition; } - public IEnumerable GetAllScoresForUsername(string username) + public IEnumerable GetAllScoresForUsername(string username) => Flow.Where(i => i.User?.Username == username); } + + private class TestGameplayLeaderboardProvider : IGameplayLeaderboardProvider + { + IBindableList IGameplayLeaderboardProvider.Scores => Scores; + public BindableList Scores { get; } = new BindableList(); + public bool IsPartial { get; set; } + } + + private class TestDrawableGameplayLeaderboardScore : IGameplayLeaderboardScore + { + public IUser User { get; } + public bool Tracked { get; } + public BindableLong TotalScore { get; } = new BindableLong(); + public BindableDouble Accuracy { get; } = new BindableDouble(); + public BindableInt Combo { get; } = new BindableInt(); + public BindableBool HasQuit { get; } = new BindableBool(); + public Bindable DisplayOrder { get; } = new BindableLong(); + public Func GetDisplayScore { get; set; } + public Colour4? TeamColour => null; + + public TestDrawableGameplayLeaderboardScore(IUser user, bool isTracked, Bindable totalScore) + { + User = user; + Tracked = isTracked; + TotalScore.BindTo(totalScore); + GetDisplayScore = _ => TotalScore.Value; + } + } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs deleted file mode 100644 index dbd14db818..0000000000 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) ppy Pty Ltd . 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 scores = new BindableList(); - - private readonly Bindable configVisibility = new Bindable(); - private readonly Bindable beatmapTabType = new Bindable(); - - 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().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 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(); - } - } -} diff --git a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs index 1eb08ad3c8..644b7f522e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs @@ -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 MultiplayerUsers = new BindableList(); - 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 multiplayerUserIds = new BindableList(); private readonly BindableDictionary watchedUserStates = new BindableDictionary(); @@ -124,11 +128,21 @@ 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); @@ -159,10 +173,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 + { + return false; + } }); } @@ -204,12 +226,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 +242,4 @@ namespace osu.Game.Tests.Visual.Multiplayer } } } + diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs index 60358dfbc4..806de68f07 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs @@ -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 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 { @@ -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().Count() == 2); + AddUntilStep("wait for load", () => leaderboard.IsLoaded); + AddUntilStep("wait for user population", () => leaderboard.ChildrenOfType().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().Single(s => s.User?.OnlineID == userId).Combo.Value == expectedCombo); + => AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType().Single(s => s.User?.OnlineID == userId).Combo.Value == expectedCombo); } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index aa98dc59db..6f6d7b31b5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -560,7 +560,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private PlayerArea getInstance(int userId) => spectatorScreen.ChildrenOfType().Single(p => p.UserId == userId); - private GameplayLeaderboardScore getLeaderboardScore(int userId) => spectatorScreen.ChildrenOfType().Single(s => s.User?.OnlineID == userId); + private DrawableGameplayLeaderboardScore getLeaderboardScore(int userId) => spectatorScreen.ChildrenOfType().Single(s => s.User?.OnlineID == userId); private int[] getPlayerIds(int count) => Enumerable.Range(PLAYER_1_ID, count).ToArray(); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 2f232a6164..53e265decb 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -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()); + Assert.That(((TestLeaderboard)LeaderboardProvider!).UserMods[TOTAL_USERS - 1], Has.One.Items); + Assert.That(((TestLeaderboard)LeaderboardProvider).UserMods[TOTAL_USERS - 1].Single(), Is.TypeOf()); }); } - private partial class TestLeaderboard : MultiplayerGameplayLeaderboard + private partial class TestLeaderboard : MultiplayerLeaderboardProvider { public Dictionary> UserMods => UserScores.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ScoreProcessor.Mods); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs index 3f1db308c0..15efde7abe 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs @@ -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); }); } diff --git a/osu.Game/Online/Leaderboards/LeaderboardManager.cs b/osu.Game/Online/Leaderboards/LeaderboardManager.cs index ff3fe39a96..121f68c12b 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardManager.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardManager.cs @@ -94,7 +94,7 @@ namespace osu.Game.Online.Leaderboards var result = new LeaderboardScores ( - 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; @@ -138,7 +138,7 @@ namespace osu.Game.Online.Leaderboards newScores = newScores.Detach().OrderByTotalScore(); - scores.Value = new LeaderboardScores(newScores, null); + scores.Value = new LeaderboardScores(newScores.ToArray(), null); if (localFetchCompletionSource != null && localFetchCompletionSource == lastFetchCompletionSource) { @@ -155,7 +155,7 @@ namespace osu.Game.Online.Leaderboards Mod[]? ExactMods ); - public record LeaderboardScores(IEnumerable TopScores, ScoreInfo? UserScore) + public record LeaderboardScores(ICollection TopScores, ScoreInfo? UserScore) { public IEnumerable AllScores { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 3d4b46f49e..d6f5529d4a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -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 isConnected = null!; private readonly TaskCompletionSource resultsReady = new TaskCompletionSource(); - private readonly MultiplayerRoomUser[] users; private LoadingLayer loadingDisplay = null!; - private MultiplayerGameplayLeaderboard multiplayerLeaderboard = null!; + + [Cached(typeof(IGameplayLeaderboardProvider))] + private readonly MultiplayerLeaderboardProvider leaderboardProvider; + + private GameplayMatchScoreDisplay teamScoreDisplay = null!; /// /// 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, } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 33c3c60ed3..85b6966eaa 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -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; /// /// Creates a new . @@ -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) { diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs similarity index 74% rename from osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs rename to osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs index f6694505dc..85f5281bef 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; using osu.Framework.Extensions.Color4Extensions; @@ -10,33 +11,39 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Containers; -using osu.Game.Users; +using osu.Game.Screens.Select.Leaderboards; +using osu.Game.Skinning; 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 Expanded = new Bindable(); - protected readonly FillFlowContainer Flow; + protected readonly FillFlowContainer 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 scores = new BindableList(); private const int max_panels = 8; /// /// Create a new leaderboard. /// - protected GameplayLeaderboard() + public DrawableGameplayLeaderboard() { - Width = GameplayLeaderboardScore.EXTENDED_WIDTH + GameplayLeaderboardScore.SHEAR_WIDTH; + Width = DrawableGameplayLeaderboardScore.EXTENDED_WIDTH + DrawableGameplayLeaderboardScore.SHEAR_WIDTH; InternalChildren = new Drawable[] { @@ -44,10 +51,10 @@ namespace osu.Game.Screens.Play.HUD { ClampExtension = 0, RelativeSizeAxes = Axes.Both, - Child = Flow = new FillFlowContainer + Child = Flow = new FillFlowContainer { RelativeSizeAxes = Axes.X, - X = GameplayLeaderboardScore.SHEAR_WIDTH, + X = DrawableGameplayLeaderboardScore.SHEAR_WIDTH, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, Spacing = new Vector2(2.5f), @@ -62,22 +69,28 @@ namespace osu.Game.Screens.Play.HUD { 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); } /// /// Adds a player to the leaderboard. /// - /// The player. - /// - /// Whether the player should be tracked on the leaderboard. - /// Set to true for the local player or a player whose replay is currently being played. - /// - public ILeaderboardScore Add(IUser? user, bool isTracked) + public void Add(IGameplayLeaderboardScore 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 +105,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 +116,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(IGameplayLeaderboardScore score) => + new DrawableGameplayLeaderboardScore(score); protected override void Update() { @@ -119,7 +130,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); @@ -171,14 +182,12 @@ 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; + orderedByScore[i].ScorePosition = i + 1 == Flow.Count && leaderboardProvider?.IsPartial == true ? null : i + 1; } sorting.Validate(); } - protected virtual bool CheckValidScorePosition(GameplayLeaderboardScore score, int position) => true; - private partial class InputDisabledScrollContainer : OsuScrollContainer { public InputDisabledScrollContainer() diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs similarity index 96% rename from osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs rename to osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs index 3d46517a68..f04d3ee492 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs @@ -22,7 +22,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 +112,27 @@ namespace osu.Game.Screens.Play.HUD private bool isFriend; /// - /// Creates a new . + /// Creates a new . /// - /// The score's player. - /// Whether the player is the local user or a replay player. - public GameplayLeaderboardScore(IUser? user, bool tracked) + public DrawableGameplayLeaderboardScore(IGameplayLeaderboardScore 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] diff --git a/osu.Game/Screens/Play/HUD/IGameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/IGameplayLeaderboardScore.cs new file mode 100644 index 0000000000..20c7b16d79 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/IGameplayLeaderboardScore.cs @@ -0,0 +1,67 @@ +// Copyright (c) ppy Pty Ltd . 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.Rulesets.Scoring; +using osu.Game.Users; + +namespace osu.Game.Screens.Play.HUD +{ + /// + /// Represents a score shown on a gameplay leaderboard. + /// The score is expected to update itself as gameplay progresses. + /// + public interface IGameplayLeaderboardScore + { + /// + /// The user playing. + /// + IUser User { get; } + + /// + /// Whether the score is being tracked. + /// Generally understood as true when this score is the score of the local user currently playing. + /// + bool Tracked { get; } + + /// + /// The current total of the score. + /// + BindableLong TotalScore { get; } + + /// + /// The current accuracy of the score. + /// + BindableDouble Accuracy { get; } + + /// + /// The current combo of the score. + /// + BindableInt Combo { get; } + + /// + /// Whether the user playing has quit. + /// + BindableBool HasQuit { get; } + + /// + /// An optional value to guarantee stable ordering. + /// Lower numbers will appear higher in cases of ties. + /// + Bindable DisplayOrder { get; } + + /// + /// A custom function which handles converting a score to a display score using a provide . + /// + /// + /// If no function is provided, will be used verbatim. + Func GetDisplayScore { get; set; } + + /// + /// The colour of the team that the user playing is on, if any. + /// + Colour4? TeamColour { get; } + } +} diff --git a/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs deleted file mode 100644 index 1a5d7fd9a8..0000000000 --- a/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) ppy Pty Ltd . 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; } - - /// - /// An optional value to guarantee stable ordering. - /// Lower numbers will appear higher in cases of ties. - /// - Bindable DisplayOrder { get; } - - /// - /// A custom function which handles converting a score to a display score using a provide . - /// - /// - /// If no function is provided, will be used verbatim. - Func GetDisplayScore { set; } - } -} diff --git a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs deleted file mode 100644 index e9bb1d2101..0000000000 --- a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (c) ppy Pty Ltd . 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 configVisibility = new Bindable(); - - private readonly Bindable scoreSource = new Bindable(); - - private readonly IUser trackingUser; - - public readonly IBindableList Scores = new BindableList(); - - [Resolved] - private ScoreProcessor scoreProcessor { get; set; } = null!; - - /// - /// Whether the leaderboard should be visible regardless of the configuration value. - /// This is true by default, but can be changed. - /// - public readonly Bindable AlwaysVisible = new Bindable(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); - } -} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b2e502406a..14bb1a1794 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -929,34 +929,30 @@ namespace osu.Game.Screens.Play #region Gameplay leaderboard + protected virtual bool ShowLeaderboard => false; + protected readonly Bindable 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; diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index a5952f3ff3..c997a67dea 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -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().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 globalScores = new Bindable(); - private readonly BindableList localScores = new BindableList(); - /// /// Add a settings group to the HUD overlay. Intended to be used by rulesets to add replay-specific settings. /// @@ -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. diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs index ed5dea98cd..e4e42e2f08 100644 --- a/osu.Game/Screens/Play/SoloPlayer.cs +++ b/osu.Game/Screens/Play/SoloPlayer.cs @@ -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 globalScores = new Bindable(); - private readonly BindableList localScores = new BindableList(); - - 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 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 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); } diff --git a/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs b/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs new file mode 100644 index 0000000000..ba3e4f728b --- /dev/null +++ b/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs @@ -0,0 +1,59 @@ +// Copyright (c) ppy Pty Ltd . 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.Screens.Play.HUD; +using osu.Game.Users; + +namespace osu.Game.Screens.Select.Leaderboards +{ + public class GameplayLeaderboardScore : IGameplayLeaderboardScore + { + public IUser User { get; } + public bool Tracked { get; } + public BindableLong TotalScore { get; } = new BindableLong(); + public BindableDouble Accuracy { get; } = new BindableDouble(); + public BindableInt Combo { get; } = new BindableInt(); + public BindableBool HasQuit { get; } = new BindableBool(); + public Bindable DisplayOrder { get; } = new BindableLong(); + public Func GetDisplayScore { get; set; } + 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; + } + } +} diff --git a/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs new file mode 100644 index 0000000000..0138f855e2 --- /dev/null +++ b/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs @@ -0,0 +1,25 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Game.Screens.Play.HUD; + +namespace osu.Game.Screens.Select.Leaderboards +{ + /// + /// Provides a leaderboard to show during gameplay. + /// + public interface IGameplayLeaderboardProvider + { + /// + /// List of all scores to display on the leaderboard. + /// + public IBindableList Scores { get; } + + /// + /// 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). + /// + bool IsPartial { get; } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/MultiSpectatorLeaderboardProvider.cs similarity index 76% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs rename to osu.Game/Screens/Select/Leaderboards/MultiSpectatorLeaderboardProvider.cs index ed92b719fc..19ae12a6ca 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/MultiSpectatorLeaderboardProvider.cs @@ -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) { } diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs similarity index 68% rename from osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs rename to osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs index 922def6174..1c2b400164 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs @@ -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,31 @@ 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 osu.Game.Screens.Play.HUD; 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 UserScores = new Dictionary(); + public IBindableList Scores => scores; + private readonly BindableList scores = new BindableList(); + protected readonly Dictionary UserScores = new Dictionary(); public readonly SortedDictionary TeamScores = new SortedDictionary(); + public bool HasTeams => TeamScores.Count > 0; + + public bool IsPartial => false; + + private readonly MultiplayerRoomUser[] users; + + private readonly Bindable scoringMode = new Bindable(); + private readonly IBindableList playingUserIds = new BindableList(); + [Resolved] - private OsuColour colours { get; set; } = null!; + private UserLookupCache userLookupCache { get; set; } = null!; [Resolved] private SpectatorClient spectatorClient { get; set; } = null!; @@ -42,31 +54,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 = null!; - - private readonly MultiplayerRoomUser[] playingUsers; - - private readonly IBindableList playingUserIds = new BindableList(); - - private bool hasTeams => TeamScores.Count > 0; - - /// - /// Construct a new leaderboard. - /// - /// IDs of all users in this match. - 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(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 +80,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 +113,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 +127,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 +148,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 +163,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); } } diff --git a/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs new file mode 100644 index 0000000000..125e8fdc9d --- /dev/null +++ b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd . 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; +using osu.Game.Screens.Play.HUD; + +namespace osu.Game.Screens.Select.Leaderboards +{ + public partial class SoloGameplayLeaderboardProvider : Component, IGameplayLeaderboardProvider + { + public bool IsPartial { get; private set; } + + public IBindableList Scores => scores; + private readonly BindableList scores = new BindableList(); + + [BackgroundDependencyLoader] + private void load(LeaderboardManager leaderboardManager, GameplayState gameplayState) + { + 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)); + } + + 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 } + }); + } + } +} From 9e2a05a1fb423b9a5a7cb173da48c958f1f46ded Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 17 Apr 2025 08:21:32 -0400 Subject: [PATCH 02/31] Update song select panel metrics in line with standard specifications and apply minor adjustments --- .../Drawables/DifficultySpectrumDisplay.cs | 2 +- osu.Game/Graphics/Carousel/CarouselItem.cs | 2 +- osu.Game/Screens/SelectV2/PanelBeatmap.cs | 13 +++++----- osu.Game/Screens/SelectV2/PanelBeatmapSet.cs | 13 +++++----- .../SelectV2/PanelBeatmapStandalone.cs | 24 ++++++++----------- .../SelectV2/PanelUpdateBeatmapButton.cs | 6 ++--- 6 files changed, 28 insertions(+), 32 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs index fc41c7c6dc..b7f4d4ca61 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs @@ -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 } diff --git a/osu.Game/Graphics/Carousel/CarouselItem.cs b/osu.Game/Graphics/Carousel/CarouselItem.cs index 223c8d9869..47e83beca6 100644 --- a/osu.Game/Graphics/Carousel/CarouselItem.cs +++ b/osu.Game/Graphics/Carousel/CarouselItem.cs @@ -11,7 +11,7 @@ namespace osu.Game.Graphics.Carousel /// public sealed class CarouselItem : IComparable { - public const float DEFAULT_HEIGHT = 50; + public const float DEFAULT_HEIGHT = 45; /// /// The model this item is representing. diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs index 6742577389..c8ae443364 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -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 } diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index 179d4d6444..7f5aa6ffe8 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -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 diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs index a0d7484587..a90a84d115 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs @@ -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 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 }, diff --git a/osu.Game/Screens/SelectV2/PanelUpdateBeatmapButton.cs b/osu.Game/Screens/SelectV2/PanelUpdateBeatmapButton.cs index 2a850321a6..4c767df9d8 100644 --- a/osu.Game/Screens/SelectV2/PanelUpdateBeatmapButton.cs +++ b/osu.Game/Screens/SelectV2/PanelUpdateBeatmapButton.cs @@ -55,7 +55,7 @@ namespace osu.Game.Screens.SelectV2 public PanelUpdateBeatmapButton() { - Size = new Vector2(75f, 22f); + Size = new Vector2(72, 22f); } private Bindable 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(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", } } From 144cec14682baba5b8397c4e9a03df150047ff27 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 17 Apr 2025 08:23:33 -0400 Subject: [PATCH 03/31] Add test cases to visualise rank display in panels --- .../SongSelectV2/TestScenePanelBeatmap.cs | 19 +++++++++++++++++++ .../TestScenePanelBeatmapStandalone.cs | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmap.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmap.cs index 53a1355fc2..c0a77553c2 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmap.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmap.cs @@ -1,17 +1,23 @@ // Copyright (c) ppy Pty Ltd . 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()) + { + AddStep($"set {rank.GetDescription()} rank", () => this.ChildrenOfType().ForEach(p => + { + p.Show(); + p.Rank = rank; + })); + } + } + protected override Drawable CreateContent() { return new FillFlowContainer diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmapStandalone.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmapStandalone.cs index 4adee17868..93e495320f 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmapStandalone.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmapStandalone.cs @@ -1,17 +1,23 @@ // Copyright (c) ppy Pty Ltd . 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()) + { + AddStep($"set {rank.GetDescription()} rank", () => this.ChildrenOfType().ForEach(p => + { + p.Show(); + p.Rank = rank; + })); + } + } + protected override Drawable CreateContent() { return new FillFlowContainer From d546bbaf8f25bbcf3f74221e0a4ec04d5a781acc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 17 Apr 2025 14:26:34 +0200 Subject: [PATCH 04/31] Attempt to fix tests --- .../MultiplayerGameplayLeaderboardTestScene.cs | 17 +++++++++++++---- .../SoloGameplayLeaderboardProvider.cs | 17 ++++++++++------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs index 644b7f522e..1481629ba0 100644 --- a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs @@ -147,10 +147,19 @@ namespace osu.Game.Tests.Visual.Multiplayer 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; + } }); } @@ -181,7 +190,7 @@ namespace osu.Game.Tests.Visual.Multiplayer spectatorClient.Verify(s => s.StopWatchingUser(user.UserID), Times.Once); return true; } - catch + catch (MockException) { return false; } diff --git a/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs index 125e8fdc9d..216fda8d9f 100644 --- a/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs +++ b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs @@ -19,11 +19,11 @@ namespace osu.Game.Screens.Select.Leaderboards private readonly BindableList scores = new BindableList(); [BackgroundDependencyLoader] - private void load(LeaderboardManager leaderboardManager, GameplayState gameplayState) + private void load(LeaderboardManager? leaderboardManager, GameplayState? gameplayState) { - var globalScores = leaderboardManager.Scores.Value; + var globalScores = leaderboardManager?.Scores.Value; - IsPartial = leaderboardManager.CurrentCriteria?.Scope != BeatmapLeaderboardScope.Local && globalScores?.TopScores.Count >= 50; + IsPartial = leaderboardManager?.CurrentCriteria?.Scope != BeatmapLeaderboardScope.Local && globalScores?.TopScores.Count >= 50; if (globalScores != null) { @@ -31,11 +31,14 @@ namespace osu.Game.Screens.Select.Leaderboards scores.Add(new GameplayLeaderboardScore(topScore, false)); } - scores.Add(new GameplayLeaderboardScore(gameplayState.Score.ScoreInfo.User, gameplayState.ScoreProcessor, true) + if (gameplayState != null) { - // Local score should always show lower than any existing scores in cases of ties. - DisplayOrder = { Value = long.MaxValue } - }); + 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 } + }); + } } } } From 8e3bace2721ca9ec66978f1ab20675bbee143608 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 16 Apr 2025 06:53:03 -0400 Subject: [PATCH 05/31] Add general constants in `SongSelect` --- osu.Game/Screens/SelectV2/SongSelect.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 67ca110dab..ca09b2a40a 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -22,6 +22,10 @@ namespace osu.Game.Screens.SelectV2 { private const float logo_scale = 0.4f; + public const float WEDGE_CONTENT_MARGIN = CORNER_RADIUS_HIDE_OFFSET + OsuGame.SCREEN_EDGE_MARGIN; + public const float CORNER_RADIUS_HIDE_OFFSET = 20f; + public const float ENTER_DURATION = 600; + private readonly ModSelectOverlay modSelectOverlay = new ModSelectOverlay(OverlayColourScheme.Aquamarine) { ShowPresets = true, From 89a8c50a45afcd6893fb2e06f48e07e66d6b80fc Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 16 Apr 2025 06:53:11 -0400 Subject: [PATCH 06/31] Add `WedgeBackground` --- osu.Game/Screens/SelectV2/WedgeBackground.cs | 54 ++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 osu.Game/Screens/SelectV2/WedgeBackground.cs diff --git a/osu.Game/Screens/SelectV2/WedgeBackground.cs b/osu.Game/Screens/SelectV2/WedgeBackground.cs new file mode 100644 index 0000000000..ecfbd51260 --- /dev/null +++ b/osu.Game/Screens/SelectV2/WedgeBackground.cs @@ -0,0 +1,54 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Overlays; + +namespace osu.Game.Screens.SelectV2 +{ + internal partial class WedgeBackground : CompositeDrawable + { + public float StartAlpha { get; init; } = 0.9f; + + public float FinalAlpha { get; init; } = 0.6f; + + public float WidthForGradient { get; init; } = 0.3f; + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + RelativeSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new Box + { + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + Width = 0.6f, + Alpha = 0.5f, + Colour = ColourInfo.GradientHorizontal(colourProvider.Background2, colourProvider.Background2.Opacity(0)), + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Width = 1 - WidthForGradient, + Colour = colourProvider.Background5.Opacity(StartAlpha), + }, + new Box + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.Both, + Width = WidthForGradient, + Colour = ColourInfo.GradientHorizontal(colourProvider.Background5.Opacity(StartAlpha), colourProvider.Background5.Opacity(FinalAlpha)), + }, + }; + } + } +} From 10c421682af3e11f03451c471cd180cb127bc98a Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 16 Apr 2025 08:15:59 -0400 Subject: [PATCH 07/31] Add popover layer in test scene base class and use half width by default --- .../SongSelectComponentsTestScene.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs index 9e9cd3505a..87c96763d5 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Testing; using osu.Game.Graphics.Cursor; using osu.Game.Overlays; @@ -27,18 +28,19 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [BackgroundDependencyLoader] private void load() { - base.Content.Child = resizeContainer = new Container + base.Content.Child = new PopoverContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Width = relativeWidth, - Children = new Drawable[] + RelativeSizeAxes = Axes.Both, + Child = resizeContainer = new Container { - Content + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Width = relativeWidth, + Child = Content } }; - AddSliderStep("change relative width", 0, 1f, 1f, v => + AddSliderStep("change relative width", 0, 1f, 0.5f, v => { if (resizeContainer != null) resizeContainer.Width = v; From bfe8cc47ecd6f4758965ddc23eb8ab7690062e86 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 17 Apr 2025 07:37:50 -0400 Subject: [PATCH 08/31] Introduce customisation properties to base song select test scene --- .../Visual/SongSelectV2/SongSelectComponentsTestScene.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs index 87c96763d5..f86ca869e1 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs @@ -25,6 +25,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 private Container? resizeContainer; private float relativeWidth; + protected virtual Anchor ComponentAnchor => Anchor.TopLeft; + protected virtual float InitialRelativeWidth => 0.5f; + [BackgroundDependencyLoader] private void load() { @@ -33,6 +36,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 RelativeSizeAxes = Axes.Both, Child = resizeContainer = new Container { + Anchor = ComponentAnchor, + Origin = ComponentAnchor, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Width = relativeWidth, @@ -40,7 +45,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } }; - AddSliderStep("change relative width", 0, 1f, 0.5f, v => + AddSliderStep("change relative width", 0, 1f, InitialRelativeWidth, v => { if (resizeContainer != null) resizeContainer.Width = v; From f93e731a5556a541b8c0fb4fb888492a1232720c Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 17 Apr 2025 08:51:53 -0400 Subject: [PATCH 09/31] Adjust sheared dropdown menu padding --- .../UserInterfaceV2/ShearedDropdown.cs | 43 ++++++++----------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs b/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs index 609f77dd7e..d77b9be2da 100644 --- a/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs +++ b/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs @@ -36,16 +36,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 } } - protected override void Update() - { - base.Update(); - - var header = (ShearedDropdownHeader)Header; - var menu = (ShearedDropdownMenu)Menu; - - menu.Padding = new MarginPadding { Left = header.LabelContainer.DrawWidth - 10f, Right = 6f }; - } - public bool OnPressed(KeyBindingPressEvent e) { if (e.Repeat) return false; @@ -62,16 +52,15 @@ namespace osu.Game.Graphics.UserInterfaceV2 protected partial class ShearedDropdownMenu : OsuDropdown.OsuDropdownMenu { - public new MarginPadding Padding - { - get => base.Padding; - set => base.Padding = value; - } - public ShearedDropdownMenu() { Shear = OsuGame.SHEAR; Margin = new MarginPadding { Top = 5f }; + Padding = new MarginPadding + { + Left = -6f, + Right = 6f + }; } protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new ShearedMenuItem(item) @@ -92,8 +81,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 public partial class ShearedDropdownHeader : DropdownHeader { - private const float corner_radius = 5f; - private LocalisableString label; protected override LocalisableString Label @@ -127,7 +114,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 public ShearedDropdownHeader() { Shear = OsuGame.SHEAR; - CornerRadius = corner_radius; + CornerRadius = ShearedButton.CORNER_RADIUS; Masking = true; Foreground.Children = new Drawable[] @@ -148,7 +135,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 { LabelContainer = new Container { - CornerRadius = corner_radius, + Depth = float.MaxValue, + CornerRadius = ShearedButton.CORNER_RADIUS, Masking = true, AutoSizeAxes = Axes.Both, Children = new Drawable[] @@ -159,8 +147,13 @@ namespace osu.Game.Graphics.UserInterfaceV2 }, labelText = new OsuSpriteText { - Margin = new MarginPadding { Horizontal = 10f, Vertical = 8f }, - Font = OsuFont.Torus.With(size: 16.8f, weight: FontWeight.SemiBold), + Margin = new MarginPadding + { + Horizontal = 10f, + // Chosen specifically so the height of these dropdowns matches ShearedToggleButton (30). + Vertical = 7f + }, + Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold), Shear = -OsuGame.SHEAR, }, }, @@ -180,7 +173,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Padding = new MarginPadding { Right = 15f }, - Font = OsuFont.Torus.With(size: 16.8f, weight: FontWeight.SemiBold), + Font = OsuFont.Style.Body, RelativeSizeAxes = Axes.X, }, chevron = new SpriteIcon @@ -197,8 +190,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 } }, }; - - AddInternal(LabelContainer.CreateProxy()); } [BackgroundDependencyLoader] @@ -223,7 +214,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 searchBar.Padding = new MarginPadding { Left = LabelContainer.DrawWidth }; // By limiting the width we avoid this box showing up as an outline around the drawables that are on top of it. - Background.Padding = new MarginPadding { Left = LabelContainer.DrawWidth - corner_radius }; + Background.Padding = new MarginPadding { Left = LabelContainer.DrawWidth - ShearedButton.CORNER_RADIUS }; } protected override bool OnHover(HoverEvent e) From a6a8e2a44fb410f3382b0ddb7f4a2b2b777a38b3 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 17 Apr 2025 07:36:03 -0400 Subject: [PATCH 10/31] Move collection dropdown test coverage to isolated test scene --- .../TestSceneCollectionDropdown.cs | 271 ++++++++++++++++++ .../SongSelect/TestSceneFilterControl.cs | 252 +--------------- 2 files changed, 272 insertions(+), 251 deletions(-) create mode 100644 osu.Game.Tests/Visual/Collections/TestSceneCollectionDropdown.cs diff --git a/osu.Game.Tests/Visual/Collections/TestSceneCollectionDropdown.cs b/osu.Game.Tests/Visual/Collections/TestSceneCollectionDropdown.cs new file mode 100644 index 0000000000..a47f3c5108 --- /dev/null +++ b/osu.Game.Tests/Visual/Collections/TestSceneCollectionDropdown.cs @@ -0,0 +1,271 @@ +// Copyright (c) ppy Pty Ltd . 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.Collections +{ + 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()); + + 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().Single().ChildrenOfType().Count(), () => Is.EqualTo(5)); + + AddStep("delete all collections", () => writeAndRefresh(r => r.RemoveAll())); + + AddAssert("check count 2", () => dropdown.ChildrenOfType().Single().ChildrenOfType().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 { "abc" })))); + assertCollectionDropdownContains("1"); + + AddStep("select collection", () => + { + InputManager.MoveMouseTo(getCollectionDropdownItemAt(1)); + InputManager.Click(MouseButton.Left); + }); + + addExpandHeaderStep(); + + AddStep("watch for filter requests", () => + { + received = false; + dropdown.ChildrenOfType().First().RequestFilter = () => received = true; + }); + + AddStep("click manage collections filter", () => + { + int lastItemIndex = dropdown.ChildrenOfType().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 action) => Realm.Write(r => + { + action(r); + r.Refresh(); + }); + + private BeatmapCollection getFirstCollection() => Realm.Run(r => r.All().First()); + + private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true) + => AddUntilStep($"collection dropdown header displays '{collectionName}'", + () => shouldDisplay == dropdown.ChildrenOfType().Any(h => h.ChildrenOfType().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().Any(i => i.ChildrenOfType().OfType().First().Text == collectionName)); + + private IconButton getAddOrRemoveButton(int index) + => getCollectionDropdownItemAt(index).ChildrenOfType().Single(); + + private void addExpandHeaderStep() => AddStep("expand header", () => + { + InputManager.MoveMouseTo(dropdown.ChildrenOfType().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().Single().ItemSource.ElementAt(index); + return dropdown.ChildrenOfType().Single(i => i.Item.Text.Value == item.CollectionName); + } + } +} diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index a639d50eee..41e44357d7 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -1,57 +1,18 @@ // Copyright (c) ppy Pty Ltd . 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 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()); - - 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().Single().ChildrenOfType().Count(), () => Is.EqualTo(5)); - - AddStep("delete all collections", () => writeAndRefresh(r => r.RemoveAll())); - - AddAssert("check count 2", () => control.ChildrenOfType().Single().ChildrenOfType().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().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 { "abc" })))); - assertCollectionDropdownContains("1"); - - AddStep("select collection", () => - { - InputManager.MoveMouseTo(getCollectionDropdownItemAt(1)); - InputManager.Click(MouseButton.Left); - }); - - addExpandHeaderStep(); - - AddStep("watch for filter requests", () => - { - received = false; - control.ChildrenOfType().First().RequestFilter = () => received = true; - }); - - AddStep("click manage collections filter", () => - { - int lastItemIndex = control.ChildrenOfType().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 action) => Realm.Write(r => - { - action(r); - r.Refresh(); - }); - - private BeatmapCollection getFirstCollection() => Realm.Run(r => r.All().First()); - - private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true) - => AddUntilStep($"collection dropdown header displays '{collectionName}'", - () => shouldDisplay == (control.ChildrenOfType().Single().ChildrenOfType().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().Any(i => i.ChildrenOfType().OfType().First().Text == collectionName)); - - private IconButton getAddOrRemoveButton(int index) - => getCollectionDropdownItemAt(index).ChildrenOfType().Single(); - - private void addExpandHeaderStep() => AddStep("expand header", () => - { - InputManager.MoveMouseTo(control.ChildrenOfType().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().Single().ItemSource.ElementAt(index); - return control.ChildrenOfType().Single(i => i.Item.Text.Value == item.CollectionName); - } } } From 54c13937af6e06cc1bc234f88627be80a52dad9a Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 17 Apr 2025 07:36:14 -0400 Subject: [PATCH 11/31] Add sheared collection dropdown --- .../TestSceneShearedCollectionDropdown.cs | 271 ++++++++++++++++++ .../Collections/ShearedCollectionDropdown.cs | 270 +++++++++++++++++ 2 files changed, 541 insertions(+) create mode 100644 osu.Game.Tests/Visual/Collections/TestSceneShearedCollectionDropdown.cs create mode 100644 osu.Game/Collections/ShearedCollectionDropdown.cs diff --git a/osu.Game.Tests/Visual/Collections/TestSceneShearedCollectionDropdown.cs b/osu.Game.Tests/Visual/Collections/TestSceneShearedCollectionDropdown.cs new file mode 100644 index 0000000000..f1afdf2019 --- /dev/null +++ b/osu.Game.Tests/Visual/Collections/TestSceneShearedCollectionDropdown.cs @@ -0,0 +1,271 @@ +// Copyright (c) ppy Pty Ltd . 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.Collections +{ + public partial class TestSceneShearedCollectionDropdown : OsuManualInputManagerTestScene + { + private BeatmapManager beatmapManager = null!; + private ShearedCollectionDropdown 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()); + + Child = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = dropdown = new ShearedCollectionDropdown + { + 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().Single().ChildrenOfType().Count(), () => Is.EqualTo(5)); + + AddStep("delete all collections", () => writeAndRefresh(r => r.RemoveAll())); + + AddAssert("check count 2", () => dropdown.ChildrenOfType().Single().ChildrenOfType().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 { "abc" })))); + assertCollectionDropdownContains("1"); + + AddStep("select collection", () => + { + InputManager.MoveMouseTo(getCollectionDropdownItemAt(1)); + InputManager.Click(MouseButton.Left); + }); + + addExpandHeaderStep(); + + AddStep("watch for filter requests", () => + { + received = false; + dropdown.ChildrenOfType().First().RequestFilter = () => received = true; + }); + + AddStep("click manage collections filter", () => + { + int lastItemIndex = dropdown.ChildrenOfType().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 action) => Realm.Write(r => + { + action(r); + r.Refresh(); + }); + + private BeatmapCollection getFirstCollection() => Realm.Run(r => r.All().First()); + + private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true) + => AddUntilStep($"collection dropdown header displays '{collectionName}'", + () => shouldDisplay == dropdown.ChildrenOfType().Any(h => h.ChildrenOfType().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().Any(i => i.ChildrenOfType().OfType().First().Text == collectionName)); + + private IconButton getAddOrRemoveButton(int index) + => getCollectionDropdownItemAt(index).ChildrenOfType().Single(); + + private void addExpandHeaderStep() => AddStep("expand header", () => + { + InputManager.MoveMouseTo(dropdown.ChildrenOfType().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().Single().ItemSource.ElementAt(index); + return dropdown.ChildrenOfType().Single(i => i.Item.Text.Value == item.CollectionName); + } + } +} diff --git a/osu.Game/Collections/ShearedCollectionDropdown.cs b/osu.Game/Collections/ShearedCollectionDropdown.cs new file mode 100644 index 0000000000..2bb2f5bfe7 --- /dev/null +++ b/osu.Game/Collections/ShearedCollectionDropdown.cs @@ -0,0 +1,270 @@ +// Copyright (c) ppy Pty Ltd . 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.Database; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osuTK; +using Realms; + +namespace osu.Game.Collections +{ + /// + /// A dropdown to select the collection to be used to filter results. + /// + public partial class ShearedCollectionDropdown : ShearedDropdown + { + /// + /// Whether to show the "manage collections..." menu item in the dropdown. + /// + protected virtual bool ShowManageCollectionsItem => true; + + public Action? RequestFilter { private get; set; } + + private readonly BindableList filters = new BindableList(); + + [Resolved] + private ManageCollectionsDialog? manageCollectionsDialog { get; set; } + + [Resolved] + private RealmAccess realm { get; set; } = null!; + + private IDisposable? realmSubscription; + + private readonly CollectionFilterMenuItem allBeatmapsItem = new AllBeatmapsCollectionFilterMenuItem(); + + public ShearedCollectionDropdown() + : base("Collection") + { + ItemSource = filters; + + Current.Value = allBeatmapsItem; + AlwaysShowSearchBar = true; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + realmSubscription = realm.RegisterForNotifications(r => r.All().OrderBy(c => c.Name), collectionsChanged); + + Current.BindValueChanged(selectionChanged); + } + + private void collectionsChanged(IRealmCollection 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? lastFiltered; + + private void selectionChanged(ValueChangedEvent 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? collection; + + [Resolved] + private IBindable beatmap { get; set; } = null!; + + public DrawableCollectionMenuItem(MenuItem item) + : base(item) + { + collection = ((DropdownMenuItem)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; + } + } + } +} From 2c690ae94c334925d0cfdad1415d00bdcb06452c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 07:38:11 +0200 Subject: [PATCH 12/31] Fix code quality --- osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs | 3 ++- osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index 23cd262dd0..5703ee754c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -7,6 +7,7 @@ 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; @@ -37,7 +38,7 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("toggle expanded", () => { - if (leaderboard != null) + if (leaderboard.IsNotNull()) leaderboard.Expanded.Value = !leaderboard.Expanded.Value; }); diff --git a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs index 85f5281bef..92baa46695 100644 --- a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Containers; using osu.Game.Screens.Select.Leaderboards; -using osu.Game.Skinning; using osuTK; using osuTK.Graphics; From 39f9eabf40b39126fbf62ad3664cca0357f92c54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 12:01:04 +0200 Subject: [PATCH 13/31] Add failing test for incorrect score position treatment --- .../Visual/Gameplay/TestSceneGameplayLeaderboard.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index 5703ee754c..e8b5326244 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -205,6 +205,9 @@ namespace osu.Game.Tests.Visual.Gameplay 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() @@ -252,6 +255,8 @@ namespace osu.Game.Tests.Visual.Gameplay public IEnumerable GetAllScoresForUsername(string username) => Flow.Where(i => i.User?.Username == username); + + public IEnumerable AllScores => Flow; } private class TestGameplayLeaderboardProvider : IGameplayLeaderboardProvider From 3ecf56b6f60538af9f31b12a8ae6c0212430268d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 12:02:02 +0200 Subject: [PATCH 14/31] Fix incorrect score position treatment if last score on partial leaderboard isn't tracked --- osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs index 92baa46695..7cfdb9631b 100644 --- a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs @@ -180,8 +180,9 @@ namespace osu.Game.Screens.Play.HUD for (int i = 0; i < Flow.Count; i++) { - Flow.SetLayoutPosition(orderedByScore[i], i); - orderedByScore[i].ScorePosition = i + 1 == Flow.Count && leaderboardProvider?.IsPartial == true ? null : i + 1; + var score = orderedByScore[i]; + Flow.SetLayoutPosition(score, i); + score.ScorePosition = i + 1 == Flow.Count && leaderboardProvider?.IsPartial == true && score.Tracked ? null : i + 1; } sorting.Validate(); From 006670c4423d96b522b5ed0c297f1818047a1c27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 12:02:21 +0200 Subject: [PATCH 15/31] Add clarification to `IsPartial` xmldoc --- .../Select/Leaderboards/IGameplayLeaderboardProvider.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs index 0138f855e2..0d88e7bf6c 100644 --- a/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs +++ b/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs @@ -20,6 +20,9 @@ namespace osu.Game.Screens.Select.Leaderboards /// 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). /// + /// + /// If this is and a tracked score is last on the leaderboard, it will show an "unknown" score position. + /// bool IsPartial { get; } } } From b80ea2647542995fda6d0c1159db42d79757b0e8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Apr 2025 19:07:44 +0900 Subject: [PATCH 16/31] Add accounting of nested group items for group panel display purposes --- osu.Game/Graphics/Carousel/CarouselItem.cs | 5 +++++ .../SelectV2/BeatmapCarouselFilterGrouping.cs | 12 +++++++++++- osu.Game/Screens/SelectV2/PanelGroup.cs | 6 +++--- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/osu.Game/Graphics/Carousel/CarouselItem.cs b/osu.Game/Graphics/Carousel/CarouselItem.cs index 223c8d9869..4904b9f13d 100644 --- a/osu.Game/Graphics/Carousel/CarouselItem.cs +++ b/osu.Game/Graphics/Carousel/CarouselItem.cs @@ -44,6 +44,11 @@ namespace osu.Game.Graphics.Carousel /// public bool IsExpanded { get; set; } + /// + /// The number of nested items underneath this header. Should only be used for headers of groups. + /// + public int NestedItemCount { get; set; } + public CarouselItem(object model) { Model = model; diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 3360437544..a628595477 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -47,7 +47,9 @@ namespace osu.Game.Screens.SelectV2 var newItems = new List(); BeatmapInfo? lastBeatmap = null; + GroupDefinition? lastGroup = null; + CarouselItem? lastGroupItem = null; HashSet? currentGroupItems = null; HashSet? currentSetItems = null; @@ -69,7 +71,7 @@ namespace osu.Game.Screens.SelectV2 groupItems[newGroup] = currentGroupItems = new HashSet(); 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(); + 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; diff --git a/osu.Game/Screens/SelectV2/PanelGroup.cs b/osu.Game/Screens/SelectV2/PanelGroup.cs index ac4857d2f3..4370146dbc 100644 --- a/osu.Game/Screens/SelectV2/PanelGroup.cs +++ b/osu.Game/Screens/SelectV2/PanelGroup.cs @@ -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"); } } } From 6d258d4ed5182341b66fe1e425dbebc1a81d0567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 12:11:42 +0200 Subject: [PATCH 17/31] Remove unnecessary interface --- .../Gameplay/TestSceneGameplayLeaderboard.cs | 30 +-------- .../Play/HUD/DrawableGameplayLeaderboard.cs | 6 +- .../HUD/DrawableGameplayLeaderboardScore.cs | 3 +- .../Play/HUD/IGameplayLeaderboardScore.cs | 67 ------------------- .../Leaderboards/GameplayLeaderboardScore.cs | 58 +++++++++++++++- .../IGameplayLeaderboardProvider.cs | 3 +- .../MultiplayerLeaderboardProvider.cs | 5 +- .../SoloGameplayLeaderboardProvider.cs | 5 +- 8 files changed, 69 insertions(+), 108 deletions(-) delete mode 100644 osu.Game/Screens/Play/HUD/IGameplayLeaderboardScore.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index e8b5326244..bef43b3108 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . 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; @@ -16,10 +15,8 @@ using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Select.Leaderboards; -using osu.Game.Users; using osuTK; namespace osu.Game.Tests.Visual.Gameplay @@ -238,7 +235,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void createLeaderboardScore(BindableLong score, APIUser user, bool isTracked = false) { - var leaderboardScore = new TestDrawableGameplayLeaderboardScore(user, isTracked, score); + var leaderboardScore = new GameplayLeaderboardScore(user, isTracked, score); leaderboardProvider.Scores.Add(leaderboardScore); } @@ -261,30 +258,9 @@ namespace osu.Game.Tests.Visual.Gameplay private class TestGameplayLeaderboardProvider : IGameplayLeaderboardProvider { - IBindableList IGameplayLeaderboardProvider.Scores => Scores; - public BindableList Scores { get; } = new BindableList(); + IBindableList IGameplayLeaderboardProvider.Scores => Scores; + public BindableList Scores { get; } = new BindableList(); public bool IsPartial { get; set; } } - - private class TestDrawableGameplayLeaderboardScore : IGameplayLeaderboardScore - { - public IUser User { get; } - public bool Tracked { get; } - public BindableLong TotalScore { get; } = new BindableLong(); - public BindableDouble Accuracy { get; } = new BindableDouble(); - public BindableInt Combo { get; } = new BindableInt(); - public BindableBool HasQuit { get; } = new BindableBool(); - public Bindable DisplayOrder { get; } = new BindableLong(); - public Func GetDisplayScore { get; set; } - public Colour4? TeamColour => null; - - public TestDrawableGameplayLeaderboardScore(IUser user, bool isTracked, Bindable totalScore) - { - User = user; - Tracked = isTracked; - TotalScore.BindTo(totalScore); - GetDisplayScore = _ => TotalScore.Value; - } - } } } diff --git a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs index 7cfdb9631b..f60d12d84f 100644 --- a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.Play.HUD [Resolved] private IGameplayLeaderboardProvider? leaderboardProvider { get; set; } - private readonly IBindableList scores = new BindableList(); + private readonly IBindableList scores = new BindableList(); private const int max_panels = 8; @@ -85,7 +85,7 @@ namespace osu.Game.Screens.Play.HUD /// /// Adds a player to the leaderboard. /// - public void Add(IGameplayLeaderboardScore score) + public void Add(GameplayLeaderboardScore score) { var drawable = CreateLeaderboardScoreDrawable(score); @@ -115,7 +115,7 @@ namespace osu.Game.Screens.Play.HUD scroll.ScrollToStart(false); } - protected virtual DrawableGameplayLeaderboardScore CreateLeaderboardScoreDrawable(IGameplayLeaderboardScore score) => + protected virtual DrawableGameplayLeaderboardScore CreateLeaderboardScoreDrawable(GameplayLeaderboardScore score) => new DrawableGameplayLeaderboardScore(score); protected override void Update() diff --git a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs index f04d3ee492..b14e31983c 100644 --- a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs @@ -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; @@ -114,7 +115,7 @@ namespace osu.Game.Screens.Play.HUD /// /// Creates a new . /// - public DrawableGameplayLeaderboardScore(IGameplayLeaderboardScore score) + public DrawableGameplayLeaderboardScore(GameplayLeaderboardScore score) { User = score.User; Tracked = score.Tracked; diff --git a/osu.Game/Screens/Play/HUD/IGameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/IGameplayLeaderboardScore.cs deleted file mode 100644 index 20c7b16d79..0000000000 --- a/osu.Game/Screens/Play/HUD/IGameplayLeaderboardScore.cs +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) ppy Pty Ltd . 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.Rulesets.Scoring; -using osu.Game.Users; - -namespace osu.Game.Screens.Play.HUD -{ - /// - /// Represents a score shown on a gameplay leaderboard. - /// The score is expected to update itself as gameplay progresses. - /// - public interface IGameplayLeaderboardScore - { - /// - /// The user playing. - /// - IUser User { get; } - - /// - /// Whether the score is being tracked. - /// Generally understood as true when this score is the score of the local user currently playing. - /// - bool Tracked { get; } - - /// - /// The current total of the score. - /// - BindableLong TotalScore { get; } - - /// - /// The current accuracy of the score. - /// - BindableDouble Accuracy { get; } - - /// - /// The current combo of the score. - /// - BindableInt Combo { get; } - - /// - /// Whether the user playing has quit. - /// - BindableBool HasQuit { get; } - - /// - /// An optional value to guarantee stable ordering. - /// Lower numbers will appear higher in cases of ties. - /// - Bindable DisplayOrder { get; } - - /// - /// A custom function which handles converting a score to a display score using a provide . - /// - /// - /// If no function is provided, will be used verbatim. - Func GetDisplayScore { get; set; } - - /// - /// The colour of the team that the user playing is on, if any. - /// - Colour4? TeamColour { get; } - } -} diff --git a/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs b/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs index ba3e4f728b..2655fd8dba 100644 --- a/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs @@ -8,21 +8,64 @@ using osu.Game.Online.Spectator; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; -using osu.Game.Screens.Play.HUD; using osu.Game.Users; namespace osu.Game.Screens.Select.Leaderboards { - public class GameplayLeaderboardScore : IGameplayLeaderboardScore + /// + /// Represents a score shown on a gameplay leaderboard. + /// The score is expected to update itself as gameplay progresses. + /// + public class GameplayLeaderboardScore { + /// + /// The user playing. + /// public IUser User { get; } + + /// + /// Whether the score is being tracked. + /// Generally understood as true when this score is the score of the local user currently playing. + /// public bool Tracked { get; } + + /// + /// The current total of the score. + /// public BindableLong TotalScore { get; } = new BindableLong(); + + /// + /// The current accuracy of the score. + /// public BindableDouble Accuracy { get; } = new BindableDouble(); + + /// + /// The current combo of the score. + /// public BindableInt Combo { get; } = new BindableInt(); + + /// + /// Whether the user playing has quit. + /// public BindableBool HasQuit { get; } = new BindableBool(); + + /// + /// An optional value to guarantee stable ordering. + /// Lower numbers will appear higher in cases of ties. + /// public Bindable DisplayOrder { get; } = new BindableLong(); + + /// + /// A custom function which handles converting a score to a display score using a provided . + /// + /// + /// If no function is provided, will be used verbatim. + /// public Func GetDisplayScore { get; set; } + + /// + /// The colour of the team that the user playing is on, if any. + /// public Colour4? TeamColour { get; init; } public GameplayLeaderboardScore(IUser user, ScoreProcessor scoreProcessor, bool tracked) @@ -55,5 +98,16 @@ namespace osu.Game.Screens.Select.Leaderboards DisplayOrder.Value = scoreInfo.OnlineID > 0 ? scoreInfo.OnlineID : scoreInfo.Date.ToUnixTimeSeconds(); GetDisplayScore = scoreInfo.GetDisplayScore; } + + /// + /// Used for testing. + /// + internal GameplayLeaderboardScore(IUser user, bool tracked, Bindable displayScore) + { + User = user; + Tracked = tracked; + TotalScore.BindTarget = displayScore; + GetDisplayScore = _ => displayScore.Value; + } } } diff --git a/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs index 0d88e7bf6c..4399c422b4 100644 --- a/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs +++ b/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; -using osu.Game.Screens.Play.HUD; namespace osu.Game.Screens.Select.Leaderboards { @@ -14,7 +13,7 @@ namespace osu.Game.Screens.Select.Leaderboards /// /// List of all scores to display on the leaderboard. /// - public IBindableList Scores { get; } + public IBindableList Scores { get; } /// /// Whether this leaderboard is a partial leaderboard (e.g. contains only the top 50 of all scores), diff --git a/osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs index 1c2b400164..edfccd0e7e 100644 --- a/osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs +++ b/osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs @@ -21,7 +21,6 @@ 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.Screens.Play.HUD; using osuTK.Graphics; namespace osu.Game.Screens.Select.Leaderboards @@ -29,8 +28,8 @@ namespace osu.Game.Screens.Select.Leaderboards [LongRunningLoad] public partial class MultiplayerLeaderboardProvider : CompositeComponent, IGameplayLeaderboardProvider { - public IBindableList Scores => scores; - private readonly BindableList scores = new BindableList(); + public IBindableList Scores => scores; + private readonly BindableList scores = new BindableList(); protected readonly Dictionary UserScores = new Dictionary(); public readonly SortedDictionary TeamScores = new SortedDictionary(); diff --git a/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs index 216fda8d9f..ac94d307c6 100644 --- a/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs +++ b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Game.Online.Leaderboards; using osu.Game.Scoring; using osu.Game.Screens.Play; -using osu.Game.Screens.Play.HUD; namespace osu.Game.Screens.Select.Leaderboards { @@ -15,8 +14,8 @@ namespace osu.Game.Screens.Select.Leaderboards { public bool IsPartial { get; private set; } - public IBindableList Scores => scores; - private readonly BindableList scores = new BindableList(); + public IBindableList Scores => scores; + private readonly BindableList scores = new BindableList(); [BackgroundDependencyLoader] private void load(LeaderboardManager? leaderboardManager, GameplayState? gameplayState) From f480765bf44b0ea797f52cc9d52cfaa56a3c50d8 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 16 Apr 2025 06:53:46 -0400 Subject: [PATCH 18/31] Add drawable wrapper for shear alignment purposes --- .../TestSceneShearAligningWrapper.cs | 132 ++++++++++++++++++ .../Containers/ShearAligningWrapper.cs | 49 +++++++ 2 files changed, 181 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneShearAligningWrapper.cs create mode 100644 osu.Game/Graphics/Containers/ShearAligningWrapper.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearAligningWrapper.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearAligningWrapper.cs new file mode 100644 index 0000000000..eb65de8fdc --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearAligningWrapper.cs @@ -0,0 +1,132 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public partial class TestSceneShearAligningWrapper : OsuTestScene + { + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + private ShearedBox first = null!; + private ShearedBox second = null!; + private ShearedBox third = null!; + + [SetUp] + public void SetUp() => Schedule(() => + { + Child = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 200f, + AutoSizeAxes = Axes.Y, + Shear = OsuGame.SHEAR, + CornerRadius = 10f, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background6, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 10f), + Children = new Drawable[] + { + new ShearAligningWrapper(first = new ShearedBox("Text 1", OsuColour.Gray(0.4f)) + { + RelativeSizeAxes = Axes.X, + Height = 30, + }), + new ShearAligningWrapper(second = new ShearedBox("Text 2", OsuColour.Gray(0.3f)) + { + RelativeSizeAxes = Axes.X, + Height = 30, + }), + new ShearAligningWrapper(third = new ShearedBox("Text 3", OsuColour.Gray(0.2f)) + { + RelativeSizeAxes = Axes.X, + Height = 30, + }), + } + } + }, + }; + }); + + [SetUpSteps] + public void SetUpSteps() + { + AddSliderStep("box 1 height", 0, 100, 30, h => + { + if (first.IsNotNull()) + first.Height = h; + }); + AddSliderStep("box 2 height", 0, 100, 30, h => + { + if (second.IsNotNull()) + second.Height = h; + }); + AddSliderStep("box 3 height", 0, 100, 30, h => + { + if (third.IsNotNull()) + third.Height = h; + }); + } + + public partial class ShearedBox : Container + { + private readonly string text; + private readonly Color4 boxColour; + + public ShearedBox(string text, Color4 boxColour) + { + this.text = text; + this.boxColour = boxColour; + } + + [BackgroundDependencyLoader] + private void load() + { + CornerRadius = 10; + Masking = true; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = boxColour, + }, + new OsuSpriteText + { + Text = text, + Colour = Color4.White, + Shear = -OsuGame.SHEAR, + Font = OsuFont.Torus.With(size: 24), + Margin = new MarginPadding { Left = 50 }, + } + }; + } + } + } +} diff --git a/osu.Game/Graphics/Containers/ShearAligningWrapper.cs b/osu.Game/Graphics/Containers/ShearAligningWrapper.cs new file mode 100644 index 0000000000..d720120b4f --- /dev/null +++ b/osu.Game/Graphics/Containers/ShearAligningWrapper.cs @@ -0,0 +1,49 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Layout; +using osuTK; + +namespace osu.Game.Graphics.Containers +{ + /// + /// Adds left padding based on direct parent to make sheared pieces in a vertical flow aligned appropriately. + /// + /// + /// See associated test scene for further demonstration. + /// + public partial class ShearAligningWrapper : CompositeDrawable + { + private readonly LayoutValue layout = new LayoutValue(Invalidation.MiscGeometry); + + public ShearAligningWrapper(Drawable drawable) + { + RelativeSizeAxes = drawable.RelativeSizeAxes; + AutoSizeAxes = Axes.Both & ~drawable.RelativeSizeAxes; + + InternalChild = drawable; + + AddLayout(layout); + } + + protected override void Update() + { + base.Update(); + + if (!layout.IsValid) + { + updateLayout(); + layout.Validate(); + } + } + + private void updateLayout() + { + float shearWidth = OsuGame.SHEAR.X * Parent!.DrawHeight; + float relativeY = Parent!.DrawHeight == 0 ? 0 : InternalChild.ToSpaceOfOtherDrawable(Vector2.Zero, Parent).Y / Parent!.DrawHeight; + Padding = new MarginPadding { Left = shearWidth * relativeY }; + } + } +} From 5791375b38bb16838e897f8935c4564661425cdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 14:03:32 +0200 Subject: [PATCH 19/31] Fix rate adjust no longer showing the rate if custom "Accidentally" removed in 6e635f124aee13d3d95d26ba10a08c321360ceb7 apparently. --- osu.Game/Rulesets/Mods/ModRateAdjust.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index 358034541c..a824731830 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -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"); } } From 20b2cc8251b7ad468a7e9d9b00b494822bd54b66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 14:21:43 +0200 Subject: [PATCH 20/31] Add failing test coverage --- osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs index 5da60966b2..4b90bec784 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs @@ -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().Any()); AddAssert("storyboard is correct width", () => Precision.AlmostEquals(storyboard?.Width ?? 0f, 480 * 16 / 9f)); } From 2761ee005dafb9f2f5eea1c5a958e2c1cdb64bb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 14:23:43 +0200 Subject: [PATCH 21/31] Fix storyboard videos not displaying Regressed with 102085668f84bd80f1717f101adc22fc7075e7fa because the stupid magic alpha transform addition was also implicitly changing the value of `IsDrawable` from false to true because that property checks for presence of any commands. Apparently past me, in his infinite wisdom, did not decide it pertinent to test that change against, you know, *a beatmap with a storyboard*. Great job, past me, good show all around. --- osu.Game/Storyboards/StoryboardSprite.cs | 2 +- osu.Game/Storyboards/StoryboardVideo.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index e10edfefe1..5b3e7c3919 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -17,7 +17,7 @@ namespace osu.Game.Storyboards private readonly List triggerGroups = new List(); public string Path { get; } - public bool IsDrawable => HasCommands; + public virtual bool IsDrawable => HasCommands; public Anchor Origin; public Vector2 InitialPosition; diff --git a/osu.Game/Storyboards/StoryboardVideo.cs b/osu.Game/Storyboards/StoryboardVideo.cs index fb4ac56e98..5a9eb533c6 100644 --- a/osu.Game/Storyboards/StoryboardVideo.cs +++ b/osu.Game/Storyboards/StoryboardVideo.cs @@ -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); } } From c29f59fcdb964288ec988eb7c41e74771848bdc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 20:02:34 +0200 Subject: [PATCH 22/31] Fix gameplay leaderboard showing scores from wrong beatmaps Kind of a big oversight this. In wanting to get the leaderboard refactors to move forward I sort of didn't realise the fact that all of the error handling related to online status and such in `BeatmapLeaderboard` kind of... can't stay there... It's also an all-or-nothing business too - moving this stuff can't really be done only in part. Not sure whether tests are warranted if it's more or less moving logic across? --- .../Online/Leaderboards/LeaderboardManager.cs | 55 +++++++++++++++++-- .../Online/Leaderboards/LeaderboardState.cs | 15 ++--- .../Select/Leaderboards/BeatmapLeaderboard.cs | 49 ++--------------- 3 files changed, 63 insertions(+), 56 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardManager.cs b/osu.Game/Online/Leaderboards/LeaderboardManager.cs index ff3fe39a96..6629781d2c 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardManager.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardManager.cs @@ -54,6 +54,9 @@ namespace osu.Game.Online.Leaderboards lastFetchCompletionSource?.TrySetCanceled(); scores.Value = null; + if (newCriteria.Beatmap == null || newCriteria.Ruleset == null) + return Task.FromResult(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(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NotLoggedIn)); + + if (!newCriteria.Ruleset.IsLegacyRuleset()) + return Task.FromResult(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.RulesetUnavailable)); + + if (newCriteria.Beatmap.OnlineID <= 0 || newCriteria.Beatmap.Status <= BeatmapOnlineStatus.Pending) + return Task.FromResult(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.BeatmapUnavailable)); + + if ((newCriteria.Scope.RequiresSupporter(newCriteria.ExactMods != null)) && !api.LocalUser.Value.IsSupporter) + return Task.FromResult(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NotSupporter)); + + if (newCriteria.Scope == BeatmapLeaderboardScope.Team && api.LocalUser.Value.Team == null) + return Task.FromResult(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NoTeam)); + var onlineFetchCompletionSource = new TaskCompletionSource(); lastFetchCompletionSource = onlineFetchCompletionSource; @@ -92,7 +110,7 @@ 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.UserScore?.CreateScoreInfo(rulesets, newCriteria.Beatmap) @@ -101,7 +119,7 @@ namespace osu.Game.Online.Leaderboards 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, 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 TopScores, ScoreInfo? UserScore) + public record LeaderboardScores { + public IEnumerable TopScores { get; } + public ScoreInfo? UserScore { get; } + public LeaderboardFailState? FailState { get; } + public IEnumerable AllScores { get @@ -168,5 +190,26 @@ namespace osu.Game.Online.Leaderboards yield return UserScore; } } + + private LeaderboardScores(IEnumerable topScores, ScoreInfo? userScore, LeaderboardFailState? failState) + { + TopScores = topScores; + UserScore = userScore; + FailState = failState; + } + + public static LeaderboardScores Success(IEnumerable 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 } } diff --git a/osu.Game/Online/Leaderboards/LeaderboardState.cs b/osu.Game/Online/Leaderboards/LeaderboardState.cs index dbd982acf2..b0b45ef04e 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardState.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardState.cs @@ -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, } } diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 2896e7eab4..f5fefa52b5 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -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> 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); From d1f7afc8edbb4e88939b3b283dae1d6fb5e0f504 Mon Sep 17 00:00:00 2001 From: schiavoanto Date: Sat, 19 Apr 2025 09:00:53 +0200 Subject: [PATCH 23/31] Change "Delete Difficulty" editor menu item type to destructive --- osu.Game/Screens/Edit/Editor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 572c4ce283..e238abbb25 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -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) }; From 99e882bfbc63b6dc17d65f6dde5738b9ccbe2263 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 20 Apr 2025 00:11:26 +0900 Subject: [PATCH 24/31] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 98ad145482..5bca6cc497 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 6949aea22e..d988adb6cf 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 267dccdd9afee0bad9742f5272684e1f10a36c2a Mon Sep 17 00:00:00 2001 From: schiavoanto Date: Sun, 20 Apr 2025 09:43:45 +0200 Subject: [PATCH 25/31] Fix slider tooltip text not updating with current value --- osu.Game/Graphics/UserInterface/OsuSliderBar.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs index 24b0e7b0f5..ca95d45042 100644 --- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs @@ -83,6 +83,6 @@ namespace osu.Game.Graphics.UserInterface channel.Play(); } - public LocalisableString GetDisplayableValue(T value) => CurrentNumber.Value.ToStandardFormattedString(max_decimal_digits, DisplayAsPercentage); + public LocalisableString GetDisplayableValue(T value) => value.ToStandardFormattedString(max_decimal_digits, DisplayAsPercentage); } } From d8df499e728cb827c4b90f75d7233d5ba75cd739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 21 Apr 2025 08:49:27 +0200 Subject: [PATCH 26/31] Allow toggling leaderboard visibility in replays Closes https://github.com/ppy/osu/issues/31744 I guess. This isn't the resolution that I had in mind for this but my hand has been basically forced by user feedback to do this, at least in the short-term. --- .../Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs | 7 ------- osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs | 9 +-------- osu.Game/Screens/Play/ReplayPlayer.cs | 1 - osu.Game/Screens/Play/SoloPlayer.cs | 1 - 4 files changed, 1 insertion(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs index dbd14db818..6b2f5767f8 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs @@ -57,7 +57,6 @@ namespace osu.Game.Tests.Visual.Gameplay Scores = { BindTarget = scores }, Anchor = Anchor.Centre, Origin = Anchor.Centre, - AlwaysVisible = { Value = false }, Expanded = { Value = true }, }; }); @@ -101,12 +100,6 @@ namespace osu.Game.Tests.Visual.Gameplay 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 createSampleScores() diff --git a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs index e9bb1d2101..b06c9b7be8 100644 --- a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs @@ -30,12 +30,6 @@ namespace osu.Game.Screens.Play.HUD [Resolved] private ScoreProcessor scoreProcessor { get; set; } = null!; - /// - /// Whether the leaderboard should be visible regardless of the configuration value. - /// This is true by default, but can be changed. - /// - public readonly Bindable AlwaysVisible = new Bindable(true); - public SoloGameplayLeaderboard(IUser trackingUser) { this.trackingUser = trackingUser; @@ -57,7 +51,6 @@ namespace osu.Game.Screens.Play.HUD // Alpha will be updated via `updateVisibility` below. Alpha = 0; - AlwaysVisible.BindValueChanged(_ => updateVisibility()); configVisibility.BindValueChanged(_ => updateVisibility(), true); } @@ -103,6 +96,6 @@ namespace osu.Game.Screens.Play.HUD } private void updateVisibility() => - this.FadeTo(AlwaysVisible.Value || configVisibility.Value ? 1 : 0, duration); + this.FadeTo(configVisibility.Value ? 1 : 0, duration); } } diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index a5952f3ff3..39f5d28e64 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -121,7 +121,6 @@ namespace osu.Game.Screens.Play protected override GameplayLeaderboard CreateGameplayLeaderboard() => new SoloGameplayLeaderboard(Score.ScoreInfo.User) { - AlwaysVisible = { Value = true }, Scores = { BindTarget = localScores } }; diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs index ed5dea98cd..eae710bd1f 100644 --- a/osu.Game/Screens/Play/SoloPlayer.cs +++ b/osu.Game/Screens/Play/SoloPlayer.cs @@ -68,7 +68,6 @@ namespace osu.Game.Screens.Play protected override GameplayLeaderboard CreateGameplayLeaderboard() => new SoloGameplayLeaderboard(Score.ScoreInfo.User) { - AlwaysVisible = { Value = false }, Scores = { BindTarget = localScores } }; From 4d08c81e8d8c2597a198f8284a1f69c3189af525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 21 Apr 2025 09:05:54 +0200 Subject: [PATCH 27/31] Move bindable list population to load complete to fix threading woes --- .../Leaderboards/SoloGameplayLeaderboardProvider.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs index ac94d307c6..5cbbb3f3b0 100644 --- a/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs +++ b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs @@ -17,9 +17,16 @@ namespace osu.Game.Screens.Select.Leaderboards public IBindableList Scores => scores; private readonly BindableList scores = new BindableList(); - [BackgroundDependencyLoader] - private void load(LeaderboardManager? leaderboardManager, GameplayState? gameplayState) + [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; From da1fc1013e07b8dafb0c409354f9d1cef971e449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 21 Apr 2025 09:15:44 +0200 Subject: [PATCH 28/31] Bring back reading from config value --- .../OnlinePlay/Multiplayer/GameplayChatDisplay.cs | 3 ++- osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs index 65f667b929..c7b65856e6 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs @@ -31,14 +31,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private readonly Bindable expandedFromTextBoxFocus = new Bindable(); 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] diff --git a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs index f60d12d84f..005cd784c4 100644 --- a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs @@ -10,6 +10,7 @@ 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.Screens.Select.Leaderboards; using osuTK; @@ -34,6 +35,7 @@ namespace osu.Game.Screens.Play.HUD private IGameplayLeaderboardProvider? leaderboardProvider { get; set; } private readonly IBindableList scores = new BindableList(); + private readonly Bindable configVisibility = new Bindable(); private const int max_panels = 8; @@ -64,6 +66,12 @@ namespace osu.Game.Screens.Play.HUD }; } + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + config.BindWith(OsuSetting.GameplayLeaderboard, configVisibility); + } + protected override void LoadComplete() { base.LoadComplete(); @@ -80,6 +88,7 @@ namespace osu.Game.Screens.Play.HUD } Scheduler.AddDelayed(sort, 1000, true); + configVisibility.BindValueChanged(_ => this.FadeTo(configVisibility.Value ? 1 : 0, 100, Easing.OutQuint), true); } /// From 78d9bd7fb4e3faea758fb1fd49184bd30442ee0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 21 Apr 2025 10:00:22 +0200 Subject: [PATCH 29/31] Fix slider repeat arrows appearing too early in editor when hit markers are enabled Closes https://github.com/ppy/osu/issues/32880 Broke in conjunction with https://github.com/ppy/osu/pull/32638 because of transforms not being applied to `DrawableSliderRepeat` but its individual pieces instead. In cross-checking with stable (visual only) the early fade in of the arrow should still apply, it just shouldn't be instantaneous as is currently ends up being with how the code is structured. --- .../Objects/Drawables/DrawableSliderRepeat.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index 9368c69ebd..8205483f82 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -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; } From ec854f7b7ffeca0aec3a42b1b1355fca1ad3204c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Apr 2025 16:56:20 +0900 Subject: [PATCH 30/31] Adjust namespaces and naming --- .../TestSceneCollectionDropdown.cs | 2 +- .../TestSceneManageCollectionsDialog.cs | 2 +- .../TestSceneCollectionDropdown.cs} | 23 ++++++++++--------- .../Music/NowPlayingCollectionDropdown.cs | 2 +- .../SelectV2/CollectionDropdown.cs} | 7 +++--- 5 files changed, 19 insertions(+), 17 deletions(-) rename osu.Game.Tests/Visual/{Collections => SongSelect}/TestSceneCollectionDropdown.cs (99%) rename osu.Game.Tests/Visual/{Collections => SongSelect}/TestSceneManageCollectionsDialog.cs (99%) rename osu.Game.Tests/Visual/{Collections/TestSceneShearedCollectionDropdown.cs => SongSelectV2/TestSceneCollectionDropdown.cs} (90%) rename osu.Game/{Collections/ShearedCollectionDropdown.cs => Screens/SelectV2/CollectionDropdown.cs} (97%) diff --git a/osu.Game.Tests/Visual/Collections/TestSceneCollectionDropdown.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneCollectionDropdown.cs similarity index 99% rename from osu.Game.Tests/Visual/Collections/TestSceneCollectionDropdown.cs rename to osu.Game.Tests/Visual/SongSelect/TestSceneCollectionDropdown.cs index a47f3c5108..fe2bf6ff5d 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneCollectionDropdown.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneCollectionDropdown.cs @@ -22,7 +22,7 @@ using osu.Game.Tests.Resources; using osuTK.Input; using Realms; -namespace osu.Game.Tests.Visual.Collections +namespace osu.Game.Tests.Visual.SongSelect { public partial class TestSceneCollectionDropdown : OsuManualInputManagerTestScene { diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneManageCollectionsDialog.cs similarity index 99% rename from osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs rename to osu.Game.Tests/Visual/SongSelect/TestSceneManageCollectionsDialog.cs index 60675018e9..4c895faf27 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneManageCollectionsDialog.cs @@ -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 { diff --git a/osu.Game.Tests/Visual/Collections/TestSceneShearedCollectionDropdown.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneCollectionDropdown.cs similarity index 90% rename from osu.Game.Tests/Visual/Collections/TestSceneShearedCollectionDropdown.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneCollectionDropdown.cs index f1afdf2019..f3c96861ed 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneShearedCollectionDropdown.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneCollectionDropdown.cs @@ -21,13 +21,14 @@ 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.Collections +namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneShearedCollectionDropdown : OsuManualInputManagerTestScene + public partial class TestSceneCollectionDropdown : OsuManualInputManagerTestScene { private BeatmapManager beatmapManager = null!; - private ShearedCollectionDropdown dropdown = null!; + private CollectionDropdown dropdown = null!; [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); @@ -51,7 +52,7 @@ namespace osu.Game.Tests.Visual.Collections { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Child = dropdown = new ShearedCollectionDropdown + Child = dropdown = new CollectionDropdown { Width = 300, Y = 100, @@ -84,11 +85,11 @@ namespace osu.Game.Tests.Visual.Collections 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().Single().ChildrenOfType().Count(), () => Is.EqualTo(5)); + AddAssert("check count 5", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(5)); AddStep("delete all collections", () => writeAndRefresh(r => r.RemoveAll())); - AddAssert("check count 2", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(2)); + AddAssert("check count 2", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(2)); } [Test] @@ -212,12 +213,12 @@ namespace osu.Game.Tests.Visual.Collections AddStep("watch for filter requests", () => { received = false; - dropdown.ChildrenOfType().First().RequestFilter = () => received = true; + dropdown.ChildrenOfType().First().RequestFilter = () => received = true; }); AddStep("click manage collections filter", () => { - int lastItemIndex = dropdown.ChildrenOfType().Single().Items.Count() - 1; + int lastItemIndex = dropdown.ChildrenOfType().Single().Items.Count() - 1; InputManager.MoveMouseTo(getCollectionDropdownItemAt(lastItemIndex)); InputManager.Click(MouseButton.Left); }); @@ -237,7 +238,7 @@ namespace osu.Game.Tests.Visual.Collections private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true) => AddUntilStep($"collection dropdown header displays '{collectionName}'", - () => shouldDisplay == dropdown.ChildrenOfType().Any(h => h.ChildrenOfType().Any(t => t.Text == collectionName))); + () => shouldDisplay == dropdown.ChildrenOfType().Any(h => h.ChildrenOfType().Any(t => t.Text == collectionName))); private void assertFirstButtonIs(IconUsage icon) => AddUntilStep($"button is {icon.Icon.ToString()}", () => getAddOrRemoveButton(1).Icon.Equals(icon)); @@ -251,7 +252,7 @@ namespace osu.Game.Tests.Visual.Collections private void addExpandHeaderStep() => AddStep("expand header", () => { - InputManager.MoveMouseTo(dropdown.ChildrenOfType().Single()); + InputManager.MoveMouseTo(dropdown.ChildrenOfType().Single()); InputManager.Click(MouseButton.Left); }); @@ -264,7 +265,7 @@ namespace osu.Game.Tests.Visual.Collections 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().Single().ItemSource.ElementAt(index); + CollectionFilterMenuItem item = dropdown.ChildrenOfType().Single().ItemSource.ElementAt(index); return dropdown.ChildrenOfType().Single(i => i.Item.Text.Value == item.CollectionName); } } diff --git a/osu.Game/Overlays/Music/NowPlayingCollectionDropdown.cs b/osu.Game/Overlays/Music/NowPlayingCollectionDropdown.cs index 0f2e9400d9..2ba222b976 100644 --- a/osu.Game/Overlays/Music/NowPlayingCollectionDropdown.cs +++ b/osu.Game/Overlays/Music/NowPlayingCollectionDropdown.cs @@ -15,7 +15,7 @@ namespace osu.Game.Overlays.Music /// /// A for use in the . /// - 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; diff --git a/osu.Game/Collections/ShearedCollectionDropdown.cs b/osu.Game/Screens/SelectV2/CollectionDropdown.cs similarity index 97% rename from osu.Game/Collections/ShearedCollectionDropdown.cs rename to osu.Game/Screens/SelectV2/CollectionDropdown.cs index 2bb2f5bfe7..a2a2ec1c93 100644 --- a/osu.Game/Collections/ShearedCollectionDropdown.cs +++ b/osu.Game/Screens/SelectV2/CollectionDropdown.cs @@ -13,6 +13,7 @@ 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; @@ -20,12 +21,12 @@ using osu.Game.Graphics.UserInterfaceV2; using osuTK; using Realms; -namespace osu.Game.Collections +namespace osu.Game.Screens.SelectV2 { /// /// A dropdown to select the collection to be used to filter results. /// - public partial class ShearedCollectionDropdown : ShearedDropdown + public partial class CollectionDropdown : ShearedDropdown // TODO: partial class under FilterControl? { /// /// Whether to show the "manage collections..." menu item in the dropdown. @@ -46,7 +47,7 @@ namespace osu.Game.Collections private readonly CollectionFilterMenuItem allBeatmapsItem = new AllBeatmapsCollectionFilterMenuItem(); - public ShearedCollectionDropdown() + public CollectionDropdown() : base("Collection") { ItemSource = filters; From 3b2382ceb0f61589092e7042057ab12b16db1085 Mon Sep 17 00:00:00 2001 From: Shavixinio Date: Tue, 22 Apr 2025 19:49:34 +0200 Subject: [PATCH 31/31] Minor fix to the description text --- osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs | 2 +- osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs b/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs index cac5b9aa6a..f2c77d6a05 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs @@ -8,6 +8,6 @@ namespace osu.Game.Rulesets.Catch.Mods { public class CatchModEasy : ModEasyWithExtraLives { - public override LocalisableString Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and three lives!"; + public override LocalisableString Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and extra lives!"; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs index 5c8cd6a5ae..275643ca44 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs @@ -8,6 +8,6 @@ namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModEasy : ModEasyWithExtraLives { - public override LocalisableString Description => @"More forgiving HP drain, less accuracy required, and three lives!"; + public override LocalisableString Description => @"More forgiving HP drain, less accuracy required, and extra lives!"; } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs b/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs index 281b36e70e..97fe0d0bf2 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs @@ -8,6 +8,6 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModEasy : ModEasyWithExtraLives { - public override LocalisableString Description => @"Larger circles, more forgiving HP drain, less accuracy required, and three lives!"; + public override LocalisableString Description => @"Larger circles, more forgiving HP drain, less accuracy required, and extra lives!"; } }