From fa49b30b5cc077f807f60fd6964bf5416f5ec845 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 21 Feb 2025 11:30:52 +0100 Subject: [PATCH 1/3] Attempt to fix spectator list showing other users in multiplayer room even if they're not spectating better Maybe closes https://github.com/ppy/osu/issues/31972. Not sure. I have no reproduction scenario to work with, no solid understanding of how the issue can happen, and if this doesn't fix it, then I'm not even entirely sure how this can ever be fixed client-side. The working theory is that not watching updates to the room provoked a situation wherein the room was temporarily not in a correct state when `WatchingUsers` changed, therefore the collection change callback failed to exclude other players in the room from display. I'm only PRing this because of the `next-release` tag on the issue. --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 80 ++++++++++++++++------ 1 file changed, 60 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 4297c62712..98b3ede874 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -2,13 +2,13 @@ // 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; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -38,8 +38,9 @@ namespace osu.Game.Screens.Play.HUD public BindableColour4 HeaderColour { get; } = new BindableColour4(Colour4.White); private BindableList watchingUsers { 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!; @@ -94,7 +95,9 @@ namespace osu.Game.Screens.Play.HUD ((IBindableList)watchingUsers).BindTo(client.WatchingUsers); ((IBindable)userPlayingState).BindTo(gameplayState.PlayingState); - watchingUsers.BindCollectionChanged(onSpectatorsChanged, true); + watchingUsers.BindCollectionChanged(onWatchingUsersChanged, true); + multiplayerClient.RoomUpdated += removePlayersFromMultiplayerRoom; + actualSpectators.BindCollectionChanged(onSpectatorsChanged, true); userPlayingState.BindValueChanged(_ => updateVisibility()); Font.BindValueChanged(_ => updateAppearance()); @@ -104,22 +107,55 @@ 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() + { + if (multiplayerClient.Room == null) + return; + // 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)); + var excludedUserIds = multiplayerClient.Room.Users.Where(u => u.State != MultiplayerUserState.Spectating).Select(u => u.UserID).ToHashSet(); + actualSpectators.RemoveAll(s => excludedUserIds.Contains(s.OnlineID)); + } + private void onSpectatorsChanged(object? sender, NotifyCollectionChangedEventArgs e) + { switch (e.Action) { case NotifyCollectionChangedAction.Add: @@ -129,9 +165,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; @@ -148,10 +181,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; @@ -167,8 +200,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++) @@ -193,7 +225,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() @@ -204,6 +236,14 @@ namespace osu.Game.Screens.Play.HUD Width = header.DrawWidth; } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (multiplayerClient.IsNotNull()) + multiplayerClient.RoomUpdated -= removePlayersFromMultiplayerRoom; + } + private partial class SpectatorListEntry : PoolableDrawable { public Bindable Current { get; } = new Bindable(); From 27ead5a383dd2bf9884d6a33ac9697909a693592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Mar 2025 09:18:49 +0100 Subject: [PATCH 2/3] Use `CurrentMatchPlayingUserIds` instead of `RoomUpdated` --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 98b3ede874..17e77f5238 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -8,7 +8,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -37,7 +36,8 @@ namespace osu.Game.Screens.Play.HUD [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextColour), nameof(SkinnableComponentStrings.TextColourDescription))] 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(); @@ -92,11 +92,14 @@ namespace osu.Game.Screens.Play.HUD { base.LoadComplete(); - ((IBindableList)watchingUsers).BindTo(client.WatchingUsers); ((IBindable)userPlayingState).BindTo(gameplayState.PlayingState); + multiplayerPlayers.BindTo(multiplayerClient.CurrentMatchPlayingUserIds); + multiplayerPlayers.BindCollectionChanged((_, _) => removePlayersFromMultiplayerRoom()); + + watchingUsers.BindTo(client.WatchingUsers); watchingUsers.BindCollectionChanged(onWatchingUsersChanged, true); - multiplayerClient.RoomUpdated += removePlayersFromMultiplayerRoom; + actualSpectators.BindCollectionChanged(onSpectatorsChanged, true); userPlayingState.BindValueChanged(_ => updateVisibility()); @@ -236,14 +239,6 @@ namespace osu.Game.Screens.Play.HUD Width = header.DrawWidth; } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (multiplayerClient.IsNotNull()) - multiplayerClient.RoomUpdated -= removePlayersFromMultiplayerRoom; - } - private partial class SpectatorListEntry : PoolableDrawable { public Bindable Current { get; } = new Bindable(); From 25108beae3fb470c123af1d2ffb1cc7fcf808269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Mar 2025 09:47:44 +0100 Subject: [PATCH 3/3] Actually use the proper list --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 17e77f5238..6479956601 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -145,16 +145,12 @@ namespace osu.Game.Screens.Play.HUD private void removePlayersFromMultiplayerRoom() { - if (multiplayerClient.Room == null) - return; - // 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. - var excludedUserIds = multiplayerClient.Room.Users.Where(u => u.State != MultiplayerUserState.Spectating).Select(u => u.UserID).ToHashSet(); - actualSpectators.RemoveAll(s => excludedUserIds.Contains(s.OnlineID)); + actualSpectators.RemoveAll(s => multiplayerPlayers.Contains(s.OnlineID)); } private void onSpectatorsChanged(object? sender, NotifyCollectionChangedEventArgs e)