diff --git a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs new file mode 100644 index 0000000000..98e6cd514b --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs @@ -0,0 +1,204 @@ +// 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.Collections.Specialized; +using System.Diagnostics; +using System.Linq; +using Moq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Configuration; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Spectator; +using osu.Game.Replays.Legacy; +using osu.Game.Rulesets.Osu.Scoring; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.Play.HUD; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public abstract class MultiplayerGameplayLeaderboardTestScene : OsuTestScene + { + protected readonly BindableList MultiplayerUsers = new BindableList(); + + protected MultiplayerGameplayLeaderboard Leaderboard { get; private set; } + + protected virtual MultiplayerRoomUser CreateUser(int userId) => new MultiplayerRoomUser(userId); + + protected abstract MultiplayerGameplayLeaderboard CreateLeaderboard(OsuScoreProcessor scoreProcessor); + + private readonly BindableList multiplayerUserIds = new BindableList(); + + private OsuConfigManager config; + + private readonly Mock spectatorClient = new Mock(); + private readonly Mock multiplayerClient = new Mock(); + + private readonly Dictionary lastHeaders = new Dictionary(); + + [BackgroundDependencyLoader] + private void load() + { + Dependencies.Cache(config = new OsuConfigManager(LocalStorage)); + Dependencies.CacheAs(spectatorClient.Object); + Dependencies.CacheAs(multiplayerClient.Object); + + // To emulate `MultiplayerClient.CurrentMatchPlayingUserIds` we need a bindable list of *only IDs*. + // This tracks the list of users 1:1. + MultiplayerUsers.BindCollectionChanged((c, e) => + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + Debug.Assert(e.NewItems != null); + + foreach (var user in e.NewItems.OfType()) + multiplayerUserIds.Add(user.UserID); + break; + + case NotifyCollectionChangedAction.Remove: + Debug.Assert(e.OldItems != null); + + foreach (var user in e.OldItems.OfType()) + multiplayerUserIds.Remove(user.UserID); + break; + + case NotifyCollectionChangedAction.Reset: + multiplayerUserIds.Clear(); + break; + } + }); + + multiplayerClient.SetupGet(c => c.CurrentMatchPlayingUserIds) + .Returns(() => multiplayerUserIds); + } + + [SetUpSteps] + public virtual void SetUpSteps() + { + AddStep("reset counts", () => spectatorClient.Invocations.Clear()); + + AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = new APIUser + { + Id = 1, + }); + + AddStep("populate users", () => + { + MultiplayerUsers.Clear(); + for (int i = 0; i < 16; i++) + MultiplayerUsers.Add(CreateUser(i)); + }); + + AddStep("create leaderboard", () => + { + Leaderboard?.Expire(); + + Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); + var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); + OsuScoreProcessor scoreProcessor = new OsuScoreProcessor(); + scoreProcessor.ApplyBeatmap(playableBeatmap); + + Child = scoreProcessor; + + LoadComponentAsync(Leaderboard = CreateLeaderboard(scoreProcessor), Add); + }); + + AddUntilStep("wait for load", () => Leaderboard.IsLoaded); + + AddStep("check watch requests were sent", () => + { + foreach (var user in MultiplayerUsers) + spectatorClient.Verify(s => s.WatchUser(user.UserID), Times.Once); + }); + } + + [Test] + public void TestScoreUpdates() + { + AddRepeatStep("update state", UpdateUserStatesRandomly, 100); + AddToggleStep("switch compact mode", expanded => Leaderboard.Expanded.Value = expanded); + } + + [Test] + public void TestUserQuit() + { + AddUntilStep("mark users quit", () => + { + if (MultiplayerUsers.Count == 0) + return true; + + MultiplayerUsers.RemoveAt(0); + return false; + }); + + AddStep("check stop watching requests were sent", () => + { + foreach (var user in MultiplayerUsers) + spectatorClient.Verify(s => s.StopWatchingUser(user.UserID), Times.Once); + }); + } + + [Test] + public void TestChangeScoringMode() + { + AddRepeatStep("update state", UpdateUserStatesRandomly, 5); + AddStep("change to classic", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Classic)); + AddStep("change to standardised", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised)); + } + + protected void UpdateUserStatesRandomly() + { + foreach (var user in MultiplayerUsers) + { + if (RNG.NextBool()) + continue; + + int userId = user.UserID; + + if (!lastHeaders.TryGetValue(userId, out var header)) + { + lastHeaders[userId] = header = new FrameHeader(new ScoreInfo + { + Statistics = new Dictionary + { + [HitResult.Miss] = 0, + [HitResult.Meh] = 0, + [HitResult.Great] = 0 + } + }); + } + + switch (RNG.Next(0, 3)) + { + case 0: + header.Combo = 0; + header.Statistics[HitResult.Miss]++; + break; + + case 1: + header.Combo++; + header.MaxCombo = Math.Max(header.MaxCombo, header.Combo); + header.Statistics[HitResult.Meh]++; + break; + + default: + header.Combo++; + header.MaxCombo = Math.Max(header.MaxCombo, header.Combo); + header.Statistics[HitResult.Great]++; + break; + } + + spectatorClient.Raise(s => s.OnNewFrames -= null, userId, new FrameDataBundle(header, new[] { new LegacyReplayFrame(Time.Current, 0, 0, ReplayButtonState.None) })); + } + } + } +} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index bcd4474876..6e4aa48b0e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -1,161 +1,22 @@ // 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.Extensions.ObjectExtensions; using osu.Framework.Graphics; -using osu.Framework.Utils; -using osu.Game.Configuration; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Multiplayer; -using osu.Game.Online.Spectator; -using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Osu.Scoring; -using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; using osu.Game.Screens.Play.HUD; -using osu.Game.Tests.Visual.OnlinePlay; -using osu.Game.Tests.Visual.Spectator; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneMultiplayerGameplayLeaderboard : MultiplayerTestScene + public class TestSceneMultiplayerGameplayLeaderboard : MultiplayerGameplayLeaderboardTestScene { - private static IEnumerable users => Enumerable.Range(0, 16); - - public new TestMultiplayerSpectatorClient SpectatorClient => (TestMultiplayerSpectatorClient)OnlinePlayDependencies?.SpectatorClient; - - private MultiplayerGameplayLeaderboard leaderboard; - private OsuConfigManager config; - - [BackgroundDependencyLoader] - private void load() + protected override MultiplayerGameplayLeaderboard CreateLeaderboard(OsuScoreProcessor scoreProcessor) { - Dependencies.Cache(config = new OsuConfigManager(LocalStorage)); - } - - public override void SetUpSteps() - { - base.SetUpSteps(); - - AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = UserLookupCache.GetUserAsync(1).GetResultSafely()); - - AddStep("create leaderboard", () => + return new MultiplayerGameplayLeaderboard(Ruleset.Value, scoreProcessor, MultiplayerUsers.ToArray()) { - leaderboard?.Expire(); - - OsuScoreProcessor scoreProcessor; - Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); - - var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); - var multiplayerUsers = new List(); - - foreach (int user in users) - { - SpectatorClient.SendStartPlay(user, Beatmap.Value.BeatmapInfo.OnlineID); - multiplayerUsers.Add(OnlinePlayDependencies.MultiplayerClient.AddUser(new APIUser { Id = user }, true)); - } - - Children = new Drawable[] - { - scoreProcessor = new OsuScoreProcessor(), - }; - - scoreProcessor.ApplyBeatmap(playableBeatmap); - - LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(Ruleset.Value, scoreProcessor, multiplayerUsers.ToArray()) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, Add); - }); - - AddUntilStep("wait for load", () => leaderboard.IsLoaded); - AddUntilStep("wait for user population", () => MultiplayerClient.CurrentMatchPlayingUserIds.Count > 0); - } - - [Test] - public void TestScoreUpdates() - { - AddRepeatStep("update state", () => SpectatorClient.RandomlyUpdateState(), 100); - AddToggleStep("switch compact mode", expanded => leaderboard.Expanded.Value = expanded); - } - - [Test] - public void TestUserQuit() - { - foreach (int user in users) - AddStep($"mark user {user} quit", () => MultiplayerClient.RemoveUser(UserLookupCache.GetUserAsync(user).GetResultSafely().AsNonNull())); - } - - [Test] - public void TestChangeScoringMode() - { - AddRepeatStep("update state", () => SpectatorClient.RandomlyUpdateState(), 5); - AddStep("change to classic", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Classic)); - AddStep("change to standardised", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised)); - } - - protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new TestDependencies(); - - protected class TestDependencies : MultiplayerTestSceneDependencies - { - protected override TestSpectatorClient CreateSpectatorClient() => new TestMultiplayerSpectatorClient(); - } - - public class TestMultiplayerSpectatorClient : TestSpectatorClient - { - private readonly Dictionary lastHeaders = new Dictionary(); - - public void RandomlyUpdateState() - { - foreach ((int userId, _) in WatchedUserStates) - { - if (RNG.NextBool()) - continue; - - if (!lastHeaders.TryGetValue(userId, out var header)) - { - lastHeaders[userId] = header = new FrameHeader(new ScoreInfo - { - Statistics = new Dictionary - { - [HitResult.Miss] = 0, - [HitResult.Meh] = 0, - [HitResult.Great] = 0 - } - }); - } - - switch (RNG.Next(0, 3)) - { - case 0: - header.Combo = 0; - header.Statistics[HitResult.Miss]++; - break; - - case 1: - header.Combo++; - header.MaxCombo = Math.Max(header.MaxCombo, header.Combo); - header.Statistics[HitResult.Meh]++; - break; - - default: - header.Combo++; - header.MaxCombo = Math.Max(header.MaxCombo, header.Combo); - header.Statistics[HitResult.Great]++; - break; - } - - ((ISpectatorClient)this).UserSentFrames(userId, new FrameDataBundle(header, new[] { new LegacyReplayFrame(Time.Current, 0, 0, ReplayButtonState.None) })); - } - } + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs index 7f5aced925..5cd3bb2f0f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs @@ -1,121 +1,56 @@ // 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.Extensions; using osu.Framework.Graphics; using osu.Framework.Utils; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; -using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.Play.HUD; -using osu.Game.Tests.Visual.OnlinePlay; -using osu.Game.Tests.Visual.Spectator; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneMultiplayerGameplayLeaderboardTeams : MultiplayerTestScene + public class TestSceneMultiplayerGameplayLeaderboardTeams : MultiplayerGameplayLeaderboardTestScene { - private static IEnumerable users => Enumerable.Range(0, 16); - - public new TestSceneMultiplayerGameplayLeaderboard.TestMultiplayerSpectatorClient SpectatorClient => - (TestSceneMultiplayerGameplayLeaderboard.TestMultiplayerSpectatorClient)OnlinePlayDependencies?.SpectatorClient; - - protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new TestDependencies(); - - protected class TestDependencies : MultiplayerTestSceneDependencies + protected override MultiplayerRoomUser CreateUser(int userId) { - protected override TestSpectatorClient CreateSpectatorClient() => new TestSceneMultiplayerGameplayLeaderboard.TestMultiplayerSpectatorClient(); + var user = base.CreateUser(userId); + user.MatchState = new TeamVersusUserState + { + TeamID = RNG.Next(0, 2) + }; + return user; } - private MultiplayerGameplayLeaderboard leaderboard; - private GameplayMatchScoreDisplay gameplayScoreDisplay; - - protected override Room CreateRoom() - { - var room = base.CreateRoom(); - room.Type.Value = MatchType.TeamVersus; - return room; - } + protected override MultiplayerGameplayLeaderboard CreateLeaderboard(OsuScoreProcessor scoreProcessor) => + new MultiplayerGameplayLeaderboard(Ruleset.Value, scoreProcessor, MultiplayerUsers.ToArray()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; public override void SetUpSteps() { base.SetUpSteps(); - AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = UserLookupCache.GetUserAsync(1).GetResultSafely()); - - AddStep("create leaderboard", () => + AddStep("Add external display components", () => { - leaderboard?.Expire(); - - OsuScoreProcessor scoreProcessor; - Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); - - var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); - var multiplayerUsers = new List(); - - foreach (int user in users) + LoadComponentAsync(new MatchScoreDisplay { - SpectatorClient.SendStartPlay(user, Beatmap.Value.BeatmapInfo.OnlineID); - var roomUser = OnlinePlayDependencies.MultiplayerClient.AddUser(new APIUser { Id = user }, true); + Team1Score = { BindTarget = Leaderboard.TeamScores[0] }, + Team2Score = { BindTarget = Leaderboard.TeamScores[1] } + }, Add); - roomUser.MatchState = new TeamVersusUserState - { - TeamID = RNG.Next(0, 2) - }; - - multiplayerUsers.Add(roomUser); - } - - Children = new Drawable[] + LoadComponentAsync(new GameplayMatchScoreDisplay { - scoreProcessor = new OsuScoreProcessor(), - }; - - scoreProcessor.ApplyBeatmap(playableBeatmap); - - LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(Ruleset.Value, scoreProcessor, multiplayerUsers.ToArray()) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, gameplayLeaderboard => - { - LoadComponentAsync(new MatchScoreDisplay - { - Team1Score = { BindTarget = leaderboard.TeamScores[0] }, - Team2Score = { BindTarget = leaderboard.TeamScores[1] } - }, Add); - - LoadComponentAsync(gameplayScoreDisplay = new GameplayMatchScoreDisplay - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Team1Score = { BindTarget = leaderboard.TeamScores[0] }, - Team2Score = { BindTarget = leaderboard.TeamScores[1] } - }, Add); - - Add(gameplayLeaderboard); - }); - }); - - AddUntilStep("wait for load", () => leaderboard.IsLoaded); - AddUntilStep("wait for user population", () => MultiplayerClient.CurrentMatchPlayingUserIds.Count > 0); - } - - [Test] - public void TestScoreUpdates() - { - AddRepeatStep("update state", () => SpectatorClient.RandomlyUpdateState(), 100); - AddToggleStep("switch compact mode", expanded => - { - leaderboard.Expanded.Value = expanded; - gameplayScoreDisplay.Expanded.Value = expanded; + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Team1Score = { BindTarget = Leaderboard.TeamScores[0] }, + Team2Score = { BindTarget = Leaderboard.TeamScores[1] }, + Expanded = { BindTarget = Leaderboard.Expanded }, + }, Add); }); } } diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 1bd1952832..18db710cee 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -42,7 +42,7 @@ namespace osu.Game.Online.Multiplayer /// /// Invoked when a user leaves the room of their own accord. /// - public event Action? UserLeft; + public virtual event Action? UserLeft; /// /// Invoked when a user was kicked from the room forcefully. @@ -107,7 +107,7 @@ namespace osu.Game.Online.Multiplayer /// /// The users in the joined which are participating in the current gameplay loop. /// - public IBindableList CurrentMatchPlayingUserIds => PlayingUserIds; + public virtual IBindableList CurrentMatchPlayingUserIds => PlayingUserIds; protected readonly BindableList PlayingUserIds = new BindableList(); diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 8f22078010..78beda6298 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -54,17 +54,17 @@ namespace osu.Game.Online.Spectator /// /// Called whenever new frames arrive from the server. /// - public event Action? OnNewFrames; + public virtual event Action? OnNewFrames; /// /// Called whenever a user starts a play session, or immediately if the user is being watched and currently in a play session. /// - public event Action? OnUserBeganPlaying; + public virtual event Action? OnUserBeganPlaying; /// /// Called whenever a user finishes a play session. /// - public event Action? OnUserFinishedPlaying; + public virtual event Action? OnUserFinishedPlaying; /// /// All users currently being watched. @@ -221,7 +221,7 @@ namespace osu.Game.Online.Spectator }); } - public void WatchUser(int userId) + public virtual void WatchUser(int userId) { Debug.Assert(ThreadSafety.IsUpdateThread);