From 9c2fed4806217e05312892a2e339bce6cebda0eb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Jul 2021 19:10:27 +0900 Subject: [PATCH 1/9] Move setup steps to `SetUpSteps` and add empty test case --- .../Multiplayer/TestSceneMultiplayer.cs | 70 +++++++++---------- 1 file changed, 32 insertions(+), 38 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index c93640e7b5..7f3e17985e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -39,11 +39,6 @@ namespace osu.Game.Tests.Visual.Multiplayer private TestMultiplayer multiplayerScreen; private TestMultiplayerClient client; - public TestSceneMultiplayer() - { - loadMultiplayer(); - } - [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { @@ -51,18 +46,43 @@ namespace osu.Game.Tests.Visual.Multiplayer Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); } - [SetUp] - public void Setup() => Schedule(() => + public override void SetUpSteps() { - beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait(); - importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); - }); + base.SetUpSteps(); + + AddStep("import beatmap", () => + { + beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait(); + importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); + }); + + AddStep("create multiplayer screen", () => multiplayerScreen = new TestMultiplayer()); + + AddStep("load dependencies", () => + { + client = new TestMultiplayerClient(multiplayerScreen.RoomManager); + + // The screen gets suspended so it stops receiving updates. + Child = client; + + LoadScreen(dependenciesScreen = new DependenciesScreen(client)); + }); + + AddUntilStep("wait for dependencies to load", () => dependenciesScreen.IsLoaded); + + AddStep("load multiplayer", () => LoadScreen(multiplayerScreen)); + AddUntilStep("wait for multiplayer to load", () => multiplayerScreen.IsLoaded); + } + + [Test] + public void TestEmpty() + { + // used to test the flow of multiplayer from visual tests. + } [Test] public void TestUserSetToIdleWhenBeatmapDeleted() { - loadMultiplayer(); - createRoom(() => new Room { Name = { Value = "Test Room" }, @@ -85,8 +105,6 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestLocalPlayDoesNotStartWhileSpectatingWithNoBeatmap() { - loadMultiplayer(); - createRoom(() => new Room { Name = { Value = "Test Room" }, @@ -123,8 +141,6 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestLocalPlayStartsWhileSpectatingWhenBeatmapBecomesAvailable() { - loadMultiplayer(); - createRoom(() => new Room { Name = { Value = "Test Room" }, @@ -167,8 +183,6 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestLeaveNavigation() { - loadMultiplayer(); - createRoom(() => new Room { Name = { Value = "Test Room" }, @@ -227,26 +241,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for join", () => client.Room != null); } - private void loadMultiplayer() - { - AddStep("create multiplayer screen", () => multiplayerScreen = new TestMultiplayer()); - - AddStep("load dependencies", () => - { - client = new TestMultiplayerClient(multiplayerScreen.RoomManager); - - // The screen gets suspended so it stops receiving updates. - Child = client; - - LoadScreen(dependenciesScreen = new DependenciesScreen(client)); - }); - - AddUntilStep("wait for dependencies to load", () => dependenciesScreen.IsLoaded); - - AddStep("load multiplayer", () => LoadScreen(multiplayerScreen)); - AddUntilStep("wait for multiplayer to load", () => multiplayerScreen.IsLoaded); - } - /// /// Used for the sole purpose of adding as a resolvable dependency. /// From 10e7c846e55a1940ce9230768de1ed0af2f0e45d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Jul 2021 19:41:00 +0900 Subject: [PATCH 2/9] Add local `UserLookupCache` to avoid online requests being fired from multiplayer tests --- osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 7f3e17985e..2bb3129f68 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -11,6 +11,7 @@ using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; @@ -39,6 +40,9 @@ namespace osu.Game.Tests.Visual.Multiplayer private TestMultiplayer multiplayerScreen; private TestMultiplayerClient client; + [Cached(typeof(UserLookupCache))] + private UserLookupCache lookupCache = new TestUserLookupCache(); + [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { From 9c311a6d8a60db4a3318478b007b4e7e09dc78c6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Jul 2021 19:56:16 +0900 Subject: [PATCH 3/9] Add ability to lookup multiple users at once to `UserLookupCache` --- osu.Game/Database/UserLookupCache.cs | 24 ++++++++++++++++++++ osu.Game/Screens/Spectate/SpectatorScreen.cs | 21 +---------------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs index 19cc211709..e5fd9c82ac 100644 --- a/osu.Game/Database/UserLookupCache.cs +++ b/osu.Game/Database/UserLookupCache.cs @@ -27,6 +27,30 @@ namespace osu.Game.Database [ItemCanBeNull] public Task GetUserAsync(int userId, CancellationToken token = default) => GetAsync(userId, token); + /// + /// Perform an API lookup on the specified users, populating a model. + /// + /// The users to lookup. + /// An optional cancellation token. + /// . + public Task GetUsersAsync(int[] userIds, CancellationToken token = default) + { + var userLookupTasks = new List>(); + + foreach (var u in userIds) + { + userLookupTasks.Add(GetUserAsync(u, token).ContinueWith(task => + { + if (!task.IsCompletedSuccessfully) + return null; + + return task.Result; + }, token)); + } + + return Task.WhenAll(userLookupTasks); + } + protected override async Task ComputeValueAsync(int lookup, CancellationToken token = default) => await queryUser(lookup).ConfigureAwait(false); diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 8fc9222f59..cd386a3d48 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -61,7 +60,7 @@ namespace osu.Game.Screens.Spectate { base.LoadComplete(); - getAllUsers().ContinueWith(users => Schedule(() => + userLookupCache.GetUsersAsync(userIds.ToArray()).ContinueWith(users => Schedule(() => { foreach (var u in users.Result) userMap[u.Id] = u; @@ -77,24 +76,6 @@ namespace osu.Game.Screens.Spectate })); } - private Task getAllUsers() - { - var userLookupTasks = new List>(); - - foreach (var u in userIds) - { - userLookupTasks.Add(userLookupCache.GetUserAsync(u).ContinueWith(task => - { - if (!task.IsCompletedSuccessfully) - return null; - - return task.Result; - })); - } - - return Task.WhenAll(userLookupTasks); - } - private void beatmapUpdated(ValueChangedEvent> e) { if (!e.NewValue.TryGetTarget(out var beatmapSet)) From 77adf687c6946471301a31a0c52e2857fe4cd5a6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Jul 2021 19:56:36 +0900 Subject: [PATCH 4/9] Consume ability to lookup multiple users in `MultiplayerGameplayLeaderboard` Avoids syncrhonously blocking on asynchronous operations (which was leading to LCA starvation in tests). --- .../Multiplayer/MultiplayerPlayer.cs | 4 +-- .../HUD/MultiplayerGameplayLeaderboard.cs | 26 +++++++++---------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 1bbe49a705..043cce4630 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -125,9 +125,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { const float padding = 44; // enough margin to avoid the hit error display. - leaderboard.Position = new Vector2( - padding, - padding + HUDOverlay.TopScoringElementsHeight); + leaderboard.Position = new Vector2(padding, padding + HUDOverlay.TopScoringElementsHeight); } private void onMatchStarted() => Scheduler.Add(() => diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index c3bfe19b29..45f871ed13 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -53,22 +53,22 @@ namespace osu.Game.Screens.Play.HUD { scoringMode = config.GetBindable(OsuSetting.ScoreDisplayMode); - foreach (var userId in playingUsers) + userLookupCache.GetUsersAsync(playingUsers.ToArray()).ContinueWith(users => Schedule(() => { - // probably won't be required in the final implementation. - var resolvedUser = userLookupCache.GetUserAsync(userId).Result; + foreach (var user in users.Result) + { + var trackedUser = CreateUserData(user.Id, scoreProcessor); + trackedUser.ScoringMode.BindTo(scoringMode); - var trackedUser = CreateUserData(userId, scoreProcessor); - trackedUser.ScoringMode.BindTo(scoringMode); + var leaderboardScore = AddPlayer(user, user.Id == api.LocalUser.Value.Id); + leaderboardScore.Accuracy.BindTo(trackedUser.Accuracy); + leaderboardScore.TotalScore.BindTo(trackedUser.Score); + leaderboardScore.Combo.BindTo(trackedUser.CurrentCombo); + leaderboardScore.HasQuit.BindTo(trackedUser.UserQuit); - var leaderboardScore = AddPlayer(resolvedUser, resolvedUser?.Id == api.LocalUser.Value.Id); - leaderboardScore.Accuracy.BindTo(trackedUser.Accuracy); - leaderboardScore.TotalScore.BindTo(trackedUser.Score); - leaderboardScore.Combo.BindTo(trackedUser.CurrentCombo); - leaderboardScore.HasQuit.BindTo(trackedUser.UserQuit); - - UserScores[userId] = trackedUser; - } + UserScores[user.Id] = trackedUser; + } + })); } protected override void LoadComplete() From 459f9a04654da0e7142601efa74503ac0e9a99ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Jul 2021 21:30:24 +0900 Subject: [PATCH 5/9] Handle nulls and fix missing documentation --- osu.Game/Database/UserLookupCache.cs | 2 +- osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs | 3 +++ osu.Game/Screens/Spectate/SpectatorScreen.cs | 5 +++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs index e5fd9c82ac..13c37ddfe9 100644 --- a/osu.Game/Database/UserLookupCache.cs +++ b/osu.Game/Database/UserLookupCache.cs @@ -32,7 +32,7 @@ namespace osu.Game.Database /// /// The users to lookup. /// An optional cancellation token. - /// . + /// The populated users. May include null results for failed retrievals. public Task GetUsersAsync(int[] userIds, CancellationToken token = default) { var userLookupTasks = new List>(); diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index 45f871ed13..a32aca51c5 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -57,6 +57,9 @@ namespace osu.Game.Screens.Play.HUD { foreach (var user in users.Result) { + if (user == null) + continue; + var trackedUser = CreateUserData(user.Id, scoreProcessor); trackedUser.ScoringMode.BindTo(scoringMode); diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index cd386a3d48..b6eafe496f 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -63,7 +63,12 @@ namespace osu.Game.Screens.Spectate userLookupCache.GetUsersAsync(userIds.ToArray()).ContinueWith(users => Schedule(() => { foreach (var u in users.Result) + { + if (u == null) + continue; + userMap[u.Id] = u; + } playingUserStates.BindTo(spectatorClient.PlayingUserStates); playingUserStates.BindCollectionChanged(onPlayingUserStatesChanged, true); From f1014af2848b9875a9f9db1e60e13e7a110a410d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Jul 2021 21:35:36 +0900 Subject: [PATCH 6/9] Move `LoadComplete` content to run after user retrieval has completed Feels safer, I think. --- osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index a32aca51c5..0c13e87f9f 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -71,13 +71,13 @@ namespace osu.Game.Screens.Play.HUD UserScores[user.Id] = trackedUser; } + + prepareDataStreams(); })); } - protected override void LoadComplete() + private void prepareDataStreams() { - base.LoadComplete(); - // BindableList handles binding in a really bad way (Clear then AddRange) so we need to do this manually.. foreach (int userId in playingUsers) { From f5ddd2a53bd0db4d49e6352a386ae62bf92d6d31 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Jul 2021 01:15:30 +0900 Subject: [PATCH 7/9] Fix critical failure causing scores to not update on the leaderboard --- .../Play/HUD/MultiplayerGameplayLeaderboard.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index 0c13e87f9f..4783ce2f40 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -71,11 +71,19 @@ namespace osu.Game.Screens.Play.HUD UserScores[user.Id] = trackedUser; } - - prepareDataStreams(); })); } + protected override void LoadComplete() + { + base.LoadComplete(); + + // this is *required* to be here due to the spectator leaderboard not correctly populating clocks if done later. + // note that running this here is probably not 100% correct (if a user quits before user population occurs for instance, + // an incorrect state will be reached). + prepareDataStreams(); + } + private void prepareDataStreams() { // BindableList handles binding in a really bad way (Clear then AddRange) so we need to do this manually.. From 1a6b8b2c7353a5d3a988db49db138900ac271761 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Jul 2021 14:53:31 +0900 Subject: [PATCH 8/9] Populate `UserScores` as early as possible to avoid weird ordering requirements --- .../HUD/MultiplayerGameplayLeaderboard.cs | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index 4783ce2f40..a10c16fcd5 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -53,6 +53,13 @@ namespace osu.Game.Screens.Play.HUD { scoringMode = config.GetBindable(OsuSetting.ScoreDisplayMode); + foreach (var userId in playingUsers) + { + var trackedUser = CreateUserData(userId, scoreProcessor); + trackedUser.ScoringMode.BindTo(scoringMode); + UserScores[userId] = trackedUser; + } + userLookupCache.GetUsersAsync(playingUsers.ToArray()).ContinueWith(users => Schedule(() => { foreach (var user in users.Result) @@ -60,16 +67,13 @@ namespace osu.Game.Screens.Play.HUD if (user == null) continue; - var trackedUser = CreateUserData(user.Id, scoreProcessor); - trackedUser.ScoringMode.BindTo(scoringMode); + var trackedUser = UserScores[user.Id]; var leaderboardScore = AddPlayer(user, user.Id == api.LocalUser.Value.Id); leaderboardScore.Accuracy.BindTo(trackedUser.Accuracy); leaderboardScore.TotalScore.BindTo(trackedUser.Score); leaderboardScore.Combo.BindTo(trackedUser.CurrentCombo); leaderboardScore.HasQuit.BindTo(trackedUser.UserQuit); - - UserScores[user.Id] = trackedUser; } })); } @@ -78,14 +82,6 @@ namespace osu.Game.Screens.Play.HUD { base.LoadComplete(); - // this is *required* to be here due to the spectator leaderboard not correctly populating clocks if done later. - // note that running this here is probably not 100% correct (if a user quits before user population occurs for instance, - // an incorrect state will be reached). - prepareDataStreams(); - } - - private void prepareDataStreams() - { // BindableList handles binding in a really bad way (Clear then AddRange) so we need to do this manually.. foreach (int userId in playingUsers) { @@ -95,6 +91,8 @@ namespace osu.Game.Screens.Play.HUD usersChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new[] { userId })); } + // bind here is to support players leaving the match. + // new players are not supported. playingUsers.BindTo(multiplayerClient.CurrentMatchPlayingUserIds); playingUsers.BindCollectionChanged(usersChanged); From 0658cfb986b4f8a06fac444075d7fa004be19cf3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Jul 2021 14:56:00 +0900 Subject: [PATCH 9/9] Throw exceptions rather than silently failing if attempting to add a clock for a non-tracked user --- .../Multiplayer/Spectate/MultiSpectatorLeaderboard.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs index ab3ead68b5..55c4270c70 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs @@ -19,7 +19,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public void AddClock(int userId, IClock clock) { if (!UserScores.TryGetValue(userId, out var data)) - return; + throw new ArgumentException(@"Provided user is not tracked by this leaderboard", nameof(userId)); ((SpectatingTrackedUserData)data).Clock = clock; } @@ -27,7 +27,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public void RemoveClock(int userId) { if (!UserScores.TryGetValue(userId, out var data)) - return; + throw new ArgumentException(@"Provided user is not tracked by this leaderboard", nameof(userId)); ((SpectatingTrackedUserData)data).Clock = null; }