22// See the LICENCE file in the repository root for full licence text.
33
44using System ;
5+ using System . Collections . Generic ;
56using System . Collections . Specialized ;
7+ using System . Linq ;
68using osu . Framework . Allocation ;
79using osu . Framework . Bindables ;
810using osu . Framework . Extensions . Color4Extensions ;
1719using osu . Game . Online . Chat ;
1820using osu . Game . Localisation . HUD ;
1921using osu . Game . Localisation . SkinComponents ;
22+ using osu . Game . Online . Multiplayer ;
2023using osu . Game . Online . Spectator ;
2124using osu . Game . Skinning ;
2225using osuTK ;
@@ -28,17 +31,17 @@ public partial class SpectatorList : CompositeDrawable, ISerialisableDrawable
2831 {
2932 private const int max_spectators_displayed = 10 ;
3033
31- public BindableList < SpectatorUser > Spectators { get ; } = new BindableList < SpectatorUser > ( ) ;
32- public Bindable < LocalUserPlayingState > UserPlayingState { get ; } = new Bindable < LocalUserPlayingState > ( ) ;
33-
3434 [ SettingSource ( typeof ( SkinnableComponentStrings ) , nameof ( SkinnableComponentStrings . Font ) , nameof ( SkinnableComponentStrings . FontDescription ) ) ]
3535 public Bindable < Typeface > Font { get ; } = new Bindable < Typeface > ( Typeface . Torus ) ;
3636
3737 [ SettingSource ( typeof ( SkinnableComponentStrings ) , nameof ( SkinnableComponentStrings . TextColour ) , nameof ( SkinnableComponentStrings . TextColourDescription ) ) ]
3838 public BindableColour4 HeaderColour { get ; } = new BindableColour4 ( Colour4 . White ) ;
3939
40- protected OsuSpriteText Header { get ; private set ; } = null ! ;
40+ private BindableList < SpectatorUser > watchingUsers { get ; } = new BindableList < SpectatorUser > ( ) ;
41+ private Bindable < LocalUserPlayingState > userPlayingState { get ; } = new Bindable < LocalUserPlayingState > ( ) ;
42+ private int displayedSpectatorCount ;
4143
44+ private OsuSpriteText header = null ! ;
4245 private FillFlowContainer mainFlow = null ! ;
4346 private FillFlowContainer < SpectatorListEntry > spectatorsFlow = null ! ;
4447 private DrawablePool < SpectatorListEntry > pool = null ! ;
@@ -49,6 +52,9 @@ public partial class SpectatorList : CompositeDrawable, ISerialisableDrawable
4952 [ Resolved ]
5053 private GameplayState gameplayState { get ; set ; } = null ! ;
5154
55+ [ Resolved ]
56+ private MultiplayerClient multiplayerClient { get ; set ; } = null ! ;
57+
5258 [ BackgroundDependencyLoader ]
5359 private void load ( OsuColour colours )
5460 {
@@ -63,7 +69,7 @@ private void load(OsuColour colours)
6369 Direction = FillDirection . Vertical ,
6470 Children = new Drawable [ ]
6571 {
66- Header = new OsuSpriteText
72+ header = new OsuSpriteText
6773 {
6874 Colour = colours . Blue0 ,
6975 Font = OsuFont . GetFont ( size : 12 , weight : FontWeight . Bold ) ,
@@ -78,18 +84,18 @@ private void load(OsuColour colours)
7884 pool = new DrawablePool < SpectatorListEntry > ( max_spectators_displayed ) ,
7985 } ;
8086
81- HeaderColour . Value = Header . Colour ;
87+ HeaderColour . Value = header . Colour ;
8288 }
8389
8490 protected override void LoadComplete ( )
8591 {
8692 base . LoadComplete ( ) ;
8793
88- ( ( IBindableList < SpectatorUser > ) Spectators ) . BindTo ( client . WatchingUsers ) ;
89- ( ( IBindable < LocalUserPlayingState > ) UserPlayingState ) . BindTo ( gameplayState . PlayingState ) ;
94+ ( ( IBindableList < SpectatorUser > ) watchingUsers ) . BindTo ( client . WatchingUsers ) ;
95+ ( ( IBindable < LocalUserPlayingState > ) userPlayingState ) . BindTo ( gameplayState . PlayingState ) ;
9096
91- Spectators . BindCollectionChanged ( onSpectatorsChanged , true ) ;
92- UserPlayingState . BindValueChanged ( _ => updateVisibility ( ) ) ;
97+ watchingUsers . BindCollectionChanged ( onSpectatorsChanged , true ) ;
98+ userPlayingState . BindValueChanged ( _ => updateVisibility ( ) ) ;
9399
94100 Font . BindValueChanged ( _ => updateAppearance ( ) ) ;
95101 HeaderColour . BindValueChanged ( _ => updateAppearance ( ) , true ) ;
@@ -100,6 +106,20 @@ protected override void LoadComplete()
100106
101107 private void onSpectatorsChanged ( object ? sender , NotifyCollectionChangedEventArgs e )
102108 {
109+ // the multiplayer gameplay leaderboard relies on calling `SpectatorClient.WatchUser()` to get updates on users' total scores.
110+ // this has an unfortunate side effect of other players showing up in `SpectatorClient.WatchingUsers`.
111+ //
112+ // we do not generally wish to display other players in the room as spectators due to that implementation detail,
113+ // therefore this code is intended to filter out those players on the client side.
114+ //
115+ // note that the way that this is done is rather specific to the multiplayer use case and therefore carries a lot of assumptions
116+ // (e.g. that the `MultiplayerRoomUser`s have the correct `State` at the point wherein they issue the `WatchUser()` calls).
117+ // 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)
118+ // 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.
119+ var excludedUserIds = new HashSet < int > ( ) ;
120+ if ( multiplayerClient . Room != null )
121+ excludedUserIds . UnionWith ( multiplayerClient . Room . Users . Where ( u => u . State != MultiplayerUserState . Spectating ) . Select ( u => u . UserID ) ) ;
122+
103123 switch ( e . Action )
104124 {
105125 case NotifyCollectionChangedAction . Add :
@@ -109,6 +129,9 @@ private void onSpectatorsChanged(object? sender, NotifyCollectionChangedEventArg
109129 var spectator = ( SpectatorUser ) e . NewItems ! [ i ] ! ;
110130 int index = Math . Max ( e . NewStartingIndex , 0 ) + i ;
111131
132+ if ( excludedUserIds . Contains ( spectator . OnlineID ) )
133+ continue ;
134+
112135 if ( index >= max_spectators_displayed )
113136 break ;
114137
@@ -125,10 +148,10 @@ private void onSpectatorsChanged(object? sender, NotifyCollectionChangedEventArg
125148 for ( int i = 0 ; i < spectatorsFlow . Count ; i ++ )
126149 spectatorsFlow . SetLayoutPosition ( spectatorsFlow [ i ] , i ) ;
127150
128- if ( Spectators . Count >= max_spectators_displayed && spectatorsFlow . Count < max_spectators_displayed )
151+ if ( watchingUsers . Count >= max_spectators_displayed && spectatorsFlow . Count < max_spectators_displayed )
129152 {
130153 for ( int i = spectatorsFlow . Count ; i < max_spectators_displayed ; i ++ )
131- addNewSpectatorToList ( i , Spectators [ i ] ) ;
154+ addNewSpectatorToList ( i , watchingUsers [ i ] ) ;
132155 }
133156
134157 break ;
@@ -144,7 +167,8 @@ private void onSpectatorsChanged(object? sender, NotifyCollectionChangedEventArg
144167 throw new NotSupportedException ( ) ;
145168 }
146169
147- Header . Text = SpectatorListStrings . SpectatorCount ( Spectators . Count ) . ToUpper ( ) ;
170+ displayedSpectatorCount = watchingUsers . Count ( s => ! excludedUserIds . Contains ( s . OnlineID ) ) ;
171+ header . Text = SpectatorListStrings . SpectatorCount ( displayedSpectatorCount ) . ToUpper ( ) ;
148172 updateVisibility ( ) ;
149173
150174 for ( int i = 0 ; i < spectatorsFlow . Count ; i ++ )
@@ -160,7 +184,7 @@ private void addNewSpectatorToList(int i, SpectatorUser spectator)
160184 var entry = pool . Get ( entry =>
161185 {
162186 entry . Current . Value = spectator ;
163- entry . UserPlayingState = UserPlayingState ;
187+ entry . UserPlayingState = userPlayingState ;
164188 } ) ;
165189
166190 spectatorsFlow . Insert ( i , entry ) ;
@@ -169,15 +193,15 @@ private void addNewSpectatorToList(int i, SpectatorUser spectator)
169193 private void updateVisibility ( )
170194 {
171195 // We don't want to show spectators when we are watching a replay.
172- mainFlow . FadeTo ( Spectators . Count > 0 && UserPlayingState . Value != LocalUserPlayingState . NotPlaying ? 1 : 0 , 250 , Easing . OutQuint ) ;
196+ mainFlow . FadeTo ( displayedSpectatorCount > 0 && userPlayingState . Value != LocalUserPlayingState . NotPlaying ? 1 : 0 , 250 , Easing . OutQuint ) ;
173197 }
174198
175199 private void updateAppearance ( )
176200 {
177- Header . Font = OsuFont . GetFont ( Font . Value , 12 , FontWeight . Bold ) ;
178- Header . Colour = HeaderColour . Value ;
201+ header . Font = OsuFont . GetFont ( Font . Value , 12 , FontWeight . Bold ) ;
202+ header . Colour = HeaderColour . Value ;
179203
180- Width = Header . DrawWidth ;
204+ Width = header . DrawWidth ;
181205 }
182206
183207 private partial class SpectatorListEntry : PoolableDrawable
0 commit comments