diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs index 8078c7b994..975c54c3f6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs @@ -20,14 +20,17 @@ 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.Multiplayer; using osu.Game.Tests.Visual.Online; namespace osu.Game.Tests.Visual.Gameplay { - public class TestSceneMultiplayerGameplayLeaderboard : OsuTestScene + public class TestSceneMultiplayerGameplayLeaderboard : MultiplayerTestScene { + private const int users = 16; + [Cached(typeof(SpectatorStreamingClient))] - private TestMultiplayerStreaming streamingClient = new TestMultiplayerStreaming(16); + private TestMultiplayerStreaming streamingClient = new TestMultiplayerStreaming(users); [Cached(typeof(UserLookupCache))] private UserLookupCache lookupCache = new TestSceneCurrentlyPlayingDisplay.TestUserLookupCache(); @@ -47,10 +50,12 @@ namespace osu.Game.Tests.Visual.Gameplay } [SetUpSteps] - public void SetUpSteps() + public override void SetUpSteps() { AddStep("create leaderboard", () => { + leaderboard?.Expire(); + OsuScoreProcessor scoreProcessor; Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); @@ -58,6 +63,9 @@ namespace osu.Game.Tests.Visual.Gameplay streamingClient.Start(Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0); + Client.PlayingUsers.Clear(); + Client.PlayingUsers.AddRange(streamingClient.PlayingUsers); + Children = new Drawable[] { scoreProcessor = new OsuScoreProcessor(), @@ -81,6 +89,12 @@ namespace osu.Game.Tests.Visual.Gameplay AddRepeatStep("update state", () => streamingClient.RandomlyUpdateState(), 100); } + [Test] + public void TestUserQuit() + { + AddRepeatStep("mark user quit", () => Client.PlayingUsers.RemoveAt(0), users); + } + public class TestMultiplayerStreaming : SpectatorStreamingClient { public new BindableList PlayingUsers => (BindableList)base.PlayingUsers; diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 51b19a8d45..83b70911c6 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -34,6 +34,7 @@ namespace osu.Game.Screens.Play.HUD public BindableDouble TotalScore { get; } = new BindableDouble(); public BindableDouble Accuracy { get; } = new BindableDouble(1); public BindableInt Combo { get; } = new BindableInt(); + public BindableBool HasQuit { get; } = new BindableBool(); private int? scorePosition; @@ -51,7 +52,7 @@ namespace osu.Game.Screens.Play.HUD positionText.Text = $"#{scorePosition.Value.FormatRank()}"; positionText.FadeTo(scorePosition.HasValue ? 1 : 0); - updateColour(); + updateState(); } } @@ -230,20 +231,31 @@ namespace osu.Game.Screens.Play.HUD TotalScore.BindValueChanged(v => scoreText.Text = v.NewValue.ToString("N0"), true); Accuracy.BindValueChanged(v => accuracyText.Text = v.NewValue.FormatAccuracy(), true); Combo.BindValueChanged(v => comboText.Text = $"{v.NewValue}x", true); + HasQuit.BindValueChanged(_ => updateState()); } protected override void LoadComplete() { base.LoadComplete(); - updateColour(); + updateState(); FinishTransforms(true); } private const double panel_transition_duration = 500; - private void updateColour() + private void updateState() { + if (HasQuit.Value) + { + // we will probably want to display this in a better way once we have a design. + // and also show states other than quit. + mainFillContainer.ResizeWidthTo(regular_width, panel_transition_duration, Easing.OutElastic); + panelColour = Color4.Gray; + textColour = Color4.White; + return; + } + if (scorePosition == 1) { mainFillContainer.ResizeWidthTo(EXTENDED_WIDTH, panel_transition_duration, Easing.OutElastic); diff --git a/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs index bc1a03c5aa..83b6f6621b 100644 --- a/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs @@ -10,5 +10,7 @@ namespace osu.Game.Screens.Play.HUD BindableDouble TotalScore { get; } BindableDouble Accuracy { get; } BindableInt Combo { get; } + + BindableBool HasQuit { get; } } } diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index c10ec9e004..00e2b8bfa7 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -2,12 +2,15 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Online.API; +using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; using osu.Game.Rulesets.Scoring; @@ -18,10 +21,21 @@ namespace osu.Game.Screens.Play.HUD { private readonly ScoreProcessor scoreProcessor; - private readonly int[] userIds; - private readonly Dictionary userScores = new Dictionary(); + [Resolved] + private SpectatorStreamingClient streamingClient { get; set; } + + [Resolved] + private StatefulMultiplayerClient multiplayerClient { get; set; } + + [Resolved] + private UserLookupCache userLookupCache { get; set; } + + private Bindable scoringMode; + + private readonly BindableList playingUsers; + /// /// Construct a new leaderboard. /// @@ -33,43 +47,68 @@ namespace osu.Game.Screens.Play.HUD this.scoreProcessor = scoreProcessor; // todo: this will likely be passed in as User instances. - this.userIds = userIds; + playingUsers = new BindableList(userIds); } - [Resolved] - private SpectatorStreamingClient streamingClient { get; set; } - - [Resolved] - private UserLookupCache userLookupCache { get; set; } - - private Bindable scoringMode; - [BackgroundDependencyLoader] private void load(OsuConfigManager config, IAPIProvider api) { streamingClient.OnNewFrames += handleIncomingFrames; - foreach (var user in userIds) + foreach (var userId in playingUsers) { - streamingClient.WatchUser(user); + streamingClient.WatchUser(userId); // probably won't be required in the final implementation. - var resolvedUser = userLookupCache.GetUserAsync(user).Result; + var resolvedUser = userLookupCache.GetUserAsync(userId).Result; var trackedUser = new TrackedUserData(); - userScores[user] = trackedUser; + userScores[userId] = trackedUser; var leaderboardScore = AddPlayer(resolvedUser, resolvedUser.Id == api.LocalUser.Value.Id); ((IBindable)leaderboardScore.Accuracy).BindTo(trackedUser.Accuracy); ((IBindable)leaderboardScore.TotalScore).BindTo(trackedUser.Score); ((IBindable)leaderboardScore.Combo).BindTo(trackedUser.CurrentCombo); + ((IBindable)leaderboardScore.HasQuit).BindTo(trackedUser.UserQuit); } scoringMode = config.GetBindable(OsuSetting.ScoreDisplayMode); scoringMode.BindValueChanged(updateAllScores, true); } + protected override void LoadComplete() + { + 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) + { + if (!multiplayerClient.PlayingUsers.Contains(userId)) + usersChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new[] { userId })); + } + + playingUsers.BindTo(multiplayerClient.PlayingUsers); + playingUsers.BindCollectionChanged(usersChanged); + } + + private void usersChanged(object sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Remove: + foreach (var userId in e.OldItems.OfType()) + { + streamingClient.StopWatchingUser(userId); + + if (userScores.TryGetValue(userId, out var trackedData)) + trackedData.MarkUserQuit(); + } + + break; + } + } + private void updateAllScores(ValueChangedEvent mode) { foreach (var trackedData in userScores.Values) @@ -91,7 +130,7 @@ namespace osu.Game.Screens.Play.HUD if (streamingClient != null) { - foreach (var user in userIds) + foreach (var user in playingUsers) { streamingClient.StopWatchingUser(user); } @@ -114,9 +153,15 @@ namespace osu.Game.Screens.Play.HUD private readonly BindableInt currentCombo = new BindableInt(); + public IBindable UserQuit => userQuit; + + private readonly BindableBool userQuit = new BindableBool(); + [CanBeNull] public FrameHeader LastHeader; + public void MarkUserQuit() => userQuit.Value = true; + public void UpdateScore(ScoreProcessor processor, ScoringMode mode) { if (LastHeader == null)