diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index c93640e7b5..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,10 +40,8 @@ namespace osu.Game.Tests.Visual.Multiplayer private TestMultiplayer multiplayerScreen; private TestMultiplayerClient client; - public TestSceneMultiplayer() - { - loadMultiplayer(); - } + [Cached(typeof(UserLookupCache))] + private UserLookupCache lookupCache = new TestUserLookupCache(); [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -51,18 +50,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 +109,6 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestLocalPlayDoesNotStartWhileSpectatingWithNoBeatmap() { - loadMultiplayer(); - createRoom(() => new Room { Name = { Value = "Test Room" }, @@ -123,8 +145,6 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestLocalPlayStartsWhileSpectatingWhenBeatmapBecomesAvailable() { - loadMultiplayer(); - createRoom(() => new Room { Name = { Value = "Test Room" }, @@ -167,8 +187,6 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestLeaveNavigation() { - loadMultiplayer(); - createRoom(() => new Room { Name = { Value = "Test Room" }, @@ -227,26 +245,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. /// diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs index 19cc211709..13c37ddfe9 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. + /// The populated users. May include null results for failed retrievals. + 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/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/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; } diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index c3bfe19b29..a10c16fcd5 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -55,20 +55,27 @@ namespace osu.Game.Screens.Play.HUD foreach (var userId in playingUsers) { - // probably won't be required in the final implementation. - var resolvedUser = userLookupCache.GetUserAsync(userId).Result; - var trackedUser = CreateUserData(userId, scoreProcessor); trackedUser.ScoringMode.BindTo(scoringMode); - - 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; } + + userLookupCache.GetUsersAsync(playingUsers.ToArray()).ContinueWith(users => Schedule(() => + { + foreach (var user in users.Result) + { + if (user == null) + continue; + + 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); + } + })); } protected override void LoadComplete() @@ -84,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); diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 8fc9222f59..b6eafe496f 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,10 +60,15 @@ namespace osu.Game.Screens.Spectate { base.LoadComplete(); - getAllUsers().ContinueWith(users => Schedule(() => + 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); @@ -77,24 +81,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))