diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 0cc4076313..0660c1c8db 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; @@ -32,9 +31,11 @@ namespace osu.Game.Screens.Play.HUD public Bindable HeaderFont { get; } = new Bindable(Typeface.Torus); public BindableColour4 HeaderColour { get; } = new BindableColour4(Colour4.White); - private BindableList watchingUsers { get; } = new BindableList(); + private IBindableList watchingUsers { get; } = new BindableList(); + private IBindableList multiplayerPlayers { get; } = new BindableList(); + private BindableList actualSpectators { get; } = new BindableList(); + private Bindable userPlayingState { get; } = new Bindable(); - private int displayedSpectatorCount; private OsuSpriteText header = null!; private FillFlowContainer mainFlow = null!; @@ -86,10 +87,15 @@ namespace osu.Game.Screens.Play.HUD { base.LoadComplete(); - ((IBindableList)watchingUsers).BindTo(client.WatchingUsers); ((IBindable)userPlayingState).BindTo(gameplayState.PlayingState); - watchingUsers.BindCollectionChanged(onSpectatorsChanged, true); + multiplayerPlayers.BindTo(multiplayerClient.CurrentMatchPlayingUserIds); + multiplayerPlayers.BindCollectionChanged((_, _) => removePlayersFromMultiplayerRoom()); + + watchingUsers.BindTo(client.WatchingUsers); + watchingUsers.BindCollectionChanged(onWatchingUsersChanged, true); + + actualSpectators.BindCollectionChanged(onSpectatorsChanged, true); userPlayingState.BindValueChanged(_ => updateVisibility()); HeaderFont.BindValueChanged(_ => updateAppearance()); @@ -99,22 +105,51 @@ namespace osu.Game.Screens.Play.HUD this.FadeInFromZero(200, Easing.OutQuint); } - private void onSpectatorsChanged(object? sender, NotifyCollectionChangedEventArgs e) + private void onWatchingUsersChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + { + for (int i = 0; i < e.NewItems!.Count; i++) + actualSpectators.Add((SpectatorUser)e.NewItems![i]!); + + break; + } + + case NotifyCollectionChangedAction.Remove: + { + for (int i = 0; i < e.OldItems!.Count; i++) + actualSpectators.Remove((SpectatorUser)e.OldItems![i]!); + + break; + } + + case NotifyCollectionChangedAction.Reset: + { + actualSpectators.Clear(); + break; + } + + default: + throw new NotSupportedException(); + } + + removePlayersFromMultiplayerRoom(); + } + + private void removePlayersFromMultiplayerRoom() { // the multiplayer gameplay leaderboard relies on calling `SpectatorClient.WatchUser()` to get updates on users' total scores. // this has an unfortunate side effect of other players showing up in `SpectatorClient.WatchingUsers`. // // we do not generally wish to display other players in the room as spectators due to that implementation detail, // therefore this code is intended to filter out those players on the client side. - // - // note that the way that this is done is rather specific to the multiplayer use case and therefore carries a lot of assumptions - // (e.g. that the `MultiplayerRoomUser`s have the correct `State` at the point wherein they issue the `WatchUser()` calls). - // the more proper way to do this (which is by subscribing to `WatchingUsers` and `RoomUpdated`, and doing a proper diff to a third list on any change of either) - // is a lot more difficult to write correctly, given that we also rely on `BindableList`'s collection changed event arguments to properly animate this component. - var excludedUserIds = new HashSet(); - if (multiplayerClient.Room != null) - excludedUserIds.UnionWith(multiplayerClient.Room.Users.Where(u => u.State != MultiplayerUserState.Spectating).Select(u => u.UserID)); + actualSpectators.RemoveAll(s => multiplayerPlayers.Contains(s.OnlineID)); + } + private void onSpectatorsChanged(object? sender, NotifyCollectionChangedEventArgs e) + { switch (e.Action) { case NotifyCollectionChangedAction.Add: @@ -124,9 +159,6 @@ namespace osu.Game.Screens.Play.HUD var spectator = (SpectatorUser)e.NewItems![i]!; int index = Math.Max(e.NewStartingIndex, 0) + i; - if (excludedUserIds.Contains(spectator.OnlineID)) - continue; - if (index >= max_spectators_displayed) break; @@ -143,10 +175,10 @@ namespace osu.Game.Screens.Play.HUD for (int i = 0; i < spectatorsFlow.Count; i++) spectatorsFlow.SetLayoutPosition(spectatorsFlow[i], i); - if (watchingUsers.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed) + if (actualSpectators.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed) { for (int i = spectatorsFlow.Count; i < max_spectators_displayed; i++) - addNewSpectatorToList(i, watchingUsers[i]); + addNewSpectatorToList(i, actualSpectators[i]); } break; @@ -162,8 +194,7 @@ namespace osu.Game.Screens.Play.HUD throw new NotSupportedException(); } - displayedSpectatorCount = watchingUsers.Count(s => !excludedUserIds.Contains(s.OnlineID)); - header.Text = SpectatorListStrings.SpectatorCount(displayedSpectatorCount).ToUpper(); + header.Text = SpectatorListStrings.SpectatorCount(actualSpectators.Count).ToUpper(); updateVisibility(); for (int i = 0; i < spectatorsFlow.Count; i++) @@ -188,7 +219,7 @@ namespace osu.Game.Screens.Play.HUD private void updateVisibility() { // We don't want to show spectators when we are watching a replay. - mainFlow.FadeTo(displayedSpectatorCount > 0 && userPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); + mainFlow.FadeTo(actualSpectators.Count > 0 && userPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); } private void updateAppearance()