2025-01-14 20:24:31 +08:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System ;
2025-02-10 18:17:17 +08:00
using System.Collections.Generic ;
2025-01-14 20:24:31 +08:00
using System.Collections.Specialized ;
2025-02-10 18:17:17 +08:00
using System.Linq ;
2025-01-14 20:24:31 +08:00
using osu.Framework.Allocation ;
using osu.Framework.Bindables ;
2025-01-16 18:05:19 +08:00
using osu.Framework.Extensions.Color4Extensions ;
2025-01-14 20:24:31 +08:00
using osu.Framework.Extensions.LocalisationExtensions ;
using osu.Framework.Graphics ;
2025-01-16 18:05:19 +08:00
using osu.Framework.Graphics.Colour ;
2025-01-14 20:24:31 +08:00
using osu.Framework.Graphics.Containers ;
using osu.Framework.Graphics.Pooling ;
using osu.Game.Configuration ;
using osu.Game.Graphics ;
using osu.Game.Graphics.Sprites ;
using osu.Game.Online.Chat ;
using osu.Game.Localisation.HUD ;
using osu.Game.Localisation.SkinComponents ;
2025-02-10 18:17:17 +08:00
using osu.Game.Online.Multiplayer ;
2025-01-14 21:44:13 +08:00
using osu.Game.Online.Spectator ;
2025-01-14 22:03:37 +08:00
using osu.Game.Skinning ;
using osuTK ;
2025-01-16 18:05:19 +08:00
using osuTK.Graphics ;
2025-01-14 20:24:31 +08:00
namespace osu.Game.Screens.Play.HUD
{
2025-01-17 18:16:35 +08:00
public partial class SpectatorList : CompositeDrawable , ISerialisableDrawable
2025-01-14 20:24:31 +08:00
{
private const int max_spectators_displayed = 10 ;
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font), nameof(SkinnableComponentStrings.FontDescription))]
public Bindable < Typeface > Font { get ; } = new Bindable < Typeface > ( Typeface . Torus ) ;
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextColour), nameof(SkinnableComponentStrings.TextColourDescription))]
public BindableColour4 HeaderColour { get ; } = new BindableColour4 ( Colour4 . White ) ;
2025-02-10 18:17:17 +08:00
private BindableList < SpectatorUser > watchingUsers { get ; } = new BindableList < SpectatorUser > ( ) ;
2025-02-10 17:41:10 +08:00
private Bindable < LocalUserPlayingState > userPlayingState { get ; } = new Bindable < LocalUserPlayingState > ( ) ;
2025-02-10 18:17:17 +08:00
private int displayedSpectatorCount ;
2025-01-14 20:24:31 +08:00
2025-02-10 17:41:10 +08:00
private OsuSpriteText header = null ! ;
2025-01-14 20:24:31 +08:00
private FillFlowContainer mainFlow = null ! ;
private FillFlowContainer < SpectatorListEntry > spectatorsFlow = null ! ;
private DrawablePool < SpectatorListEntry > pool = null ! ;
2025-01-21 17:02:16 +08:00
[Resolved]
private SpectatorClient client { get ; set ; } = null ! ;
[Resolved]
private GameplayState gameplayState { get ; set ; } = null ! ;
2025-02-10 18:17:17 +08:00
[Resolved]
private MultiplayerClient multiplayerClient { get ; set ; } = null ! ;
2025-01-14 20:24:31 +08:00
[BackgroundDependencyLoader]
2025-01-21 17:02:16 +08:00
private void load ( OsuColour colours )
2025-01-14 20:24:31 +08:00
{
2025-01-16 18:02:14 +08:00
AutoSizeAxes = Axes . Y ;
2025-01-14 20:24:31 +08:00
2025-01-14 22:03:37 +08:00
InternalChildren = new [ ]
2025-01-14 20:24:31 +08:00
{
2025-01-14 22:03:37 +08:00
Empty ( ) . With ( t = > t . Size = new Vector2 ( 100 , 50 ) ) ,
2025-01-14 20:24:31 +08:00
mainFlow = new FillFlowContainer
{
AutoSizeAxes = Axes . Both ,
Direction = FillDirection . Vertical ,
Children = new Drawable [ ]
{
2025-02-10 17:41:10 +08:00
header = new OsuSpriteText
2025-01-14 20:24:31 +08:00
{
Colour = colours . Blue0 ,
Font = OsuFont . GetFont ( size : 12 , weight : FontWeight . Bold ) ,
} ,
spectatorsFlow = new FillFlowContainer < SpectatorListEntry >
{
AutoSizeAxes = Axes . Both ,
Direction = FillDirection . Vertical ,
}
}
} ,
pool = new DrawablePool < SpectatorListEntry > ( max_spectators_displayed ) ,
} ;
2025-02-10 17:41:10 +08:00
HeaderColour . Value = header . Colour ;
2025-01-14 20:24:31 +08:00
}
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
2025-02-10 18:17:17 +08:00
( ( IBindableList < SpectatorUser > ) watchingUsers ) . BindTo ( client . WatchingUsers ) ;
2025-02-10 17:41:10 +08:00
( ( IBindable < LocalUserPlayingState > ) userPlayingState ) . BindTo ( gameplayState . PlayingState ) ;
2025-01-21 17:02:16 +08:00
2025-02-10 18:17:17 +08:00
watchingUsers . BindCollectionChanged ( onSpectatorsChanged , true ) ;
2025-02-10 17:41:10 +08:00
userPlayingState . BindValueChanged ( _ = > updateVisibility ( ) ) ;
2025-01-14 20:24:31 +08:00
Font . BindValueChanged ( _ = > updateAppearance ( ) ) ;
HeaderColour . BindValueChanged ( _ = > updateAppearance ( ) , true ) ;
FinishTransforms ( true ) ;
2025-01-16 18:23:54 +08:00
this . FadeInFromZero ( 200 , Easing . OutQuint ) ;
2025-01-14 20:24:31 +08:00
}
private void onSpectatorsChanged ( object? sender , NotifyCollectionChangedEventArgs e )
{
2025-02-10 18:17:17 +08:00
// 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`.
2025-02-10 20:48:27 +08:00
//
2025-02-10 18:17:17 +08:00
// 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.
2025-02-10 20:48:27 +08:00
//
2025-02-10 18:17:17 +08:00
// 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 < int > ( ) ;
if ( multiplayerClient . Room ! = null )
excludedUserIds . UnionWith ( multiplayerClient . Room . Users . Where ( u = > u . State ! = MultiplayerUserState . Spectating ) . Select ( u = > u . UserID ) ) ;
2025-01-14 20:24:31 +08:00
switch ( e . Action )
{
case NotifyCollectionChangedAction . Add :
{
for ( int i = 0 ; i < e . NewItems ! . Count ; i + + )
{
2025-01-14 21:44:13 +08:00
var spectator = ( SpectatorUser ) e . NewItems ! [ i ] ! ;
2025-01-16 21:29:41 +08:00
int index = Math . Max ( e . NewStartingIndex , 0 ) + i ;
2025-01-14 20:24:31 +08:00
2025-02-10 18:17:17 +08:00
if ( excludedUserIds . Contains ( spectator . OnlineID ) )
continue ;
2025-01-14 20:24:31 +08:00
if ( index > = max_spectators_displayed )
break ;
2025-01-16 18:23:54 +08:00
addNewSpectatorToList ( index , spectator ) ;
2025-01-14 20:24:31 +08:00
}
break ;
}
case NotifyCollectionChangedAction . Remove :
{
spectatorsFlow . RemoveAll ( entry = > e . OldItems ! . Contains ( entry . Current . Value ) , false ) ;
for ( int i = 0 ; i < spectatorsFlow . Count ; i + + )
spectatorsFlow . SetLayoutPosition ( spectatorsFlow [ i ] , i ) ;
2025-02-10 18:17:17 +08:00
if ( watchingUsers . Count > = max_spectators_displayed & & spectatorsFlow . Count < max_spectators_displayed )
2025-01-14 20:24:31 +08:00
{
for ( int i = spectatorsFlow . Count ; i < max_spectators_displayed ; i + + )
2025-02-10 18:17:17 +08:00
addNewSpectatorToList ( i , watchingUsers [ i ] ) ;
2025-01-14 20:24:31 +08:00
}
break ;
}
case NotifyCollectionChangedAction . Reset :
{
spectatorsFlow . Clear ( false ) ;
break ;
}
default :
throw new NotSupportedException ( ) ;
}
2025-02-10 18:17:17 +08:00
displayedSpectatorCount = watchingUsers . Count ( s = > ! excludedUserIds . Contains ( s . OnlineID ) ) ;
header . Text = SpectatorListStrings . SpectatorCount ( displayedSpectatorCount ) . ToUpper ( ) ;
2025-01-14 20:24:31 +08:00
updateVisibility ( ) ;
2025-01-16 18:05:19 +08:00
for ( int i = 0 ; i < spectatorsFlow . Count ; i + + )
{
spectatorsFlow [ i ] . Colour = i < max_spectators_displayed - 1
? Color4 . White
: ColourInfo . GradientVertical ( Color4 . White , Color4 . White . Opacity ( 0 ) ) ;
}
2025-01-14 20:24:31 +08:00
}
2025-01-16 22:13:04 +08:00
private void addNewSpectatorToList ( int i , SpectatorUser spectator )
2025-01-16 18:23:54 +08:00
{
var entry = pool . Get ( entry = >
{
entry . Current . Value = spectator ;
2025-02-10 17:41:10 +08:00
entry . UserPlayingState = userPlayingState ;
2025-01-16 18:23:54 +08:00
} ) ;
spectatorsFlow . Insert ( i , entry ) ;
2025-01-14 20:24:31 +08:00
}
private void updateVisibility ( )
{
2025-01-17 16:14:06 +08:00
// We don't want to show spectators when we are watching a replay.
2025-02-10 18:17:17 +08:00
mainFlow . FadeTo ( displayedSpectatorCount > 0 & & userPlayingState . Value ! = LocalUserPlayingState . NotPlaying ? 1 : 0 , 250 , Easing . OutQuint ) ;
2025-01-14 20:24:31 +08:00
}
private void updateAppearance ( )
{
2025-02-10 17:41:10 +08:00
header . Font = OsuFont . GetFont ( Font . Value , 12 , FontWeight . Bold ) ;
header . Colour = HeaderColour . Value ;
2025-01-16 18:02:14 +08:00
2025-02-10 17:41:10 +08:00
Width = header . DrawWidth ;
2025-01-14 20:24:31 +08:00
}
private partial class SpectatorListEntry : PoolableDrawable
{
2025-01-14 21:44:13 +08:00
public Bindable < SpectatorUser > Current { get ; } = new Bindable < SpectatorUser > ( ) ;
2025-01-14 20:24:31 +08:00
private readonly BindableWithCurrent < LocalUserPlayingState > current = new BindableWithCurrent < LocalUserPlayingState > ( ) ;
public Bindable < LocalUserPlayingState > UserPlayingState
{
get = > current . Current ;
set = > current . Current = value ;
}
private OsuSpriteText username = null ! ;
private DrawableLinkCompiler ? linkCompiler ;
[Resolved]
private OsuGame ? game { get ; set ; }
[BackgroundDependencyLoader]
private void load ( )
{
AutoSizeAxes = Axes . Both ;
InternalChildren = new Drawable [ ]
{
username = new OsuSpriteText ( ) ,
} ;
}
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
UserPlayingState . BindValueChanged ( _ = > updateEnabledState ( ) ) ;
Current . BindValueChanged ( _ = > updateState ( ) , true ) ;
}
2025-01-16 18:23:54 +08:00
protected override void PrepareForUse ( )
{
base . PrepareForUse ( ) ;
username . MoveToX ( 10 )
. Then ( )
. MoveToX ( 0 , 400 , Easing . OutQuint ) ;
this . FadeInFromZero ( 400 , Easing . OutQuint ) ;
}
2025-01-14 20:24:31 +08:00
private void updateState ( )
{
username . Text = Current . Value . Username ;
linkCompiler ? . Expire ( ) ;
AddInternal ( linkCompiler = new DrawableLinkCompiler ( [ username ] )
{
IdleColour = Colour4 . White ,
Action = ( ) = > game ? . HandleLink ( new LinkDetails ( LinkAction . OpenUserProfile , Current . Value ) ) ,
} ) ;
updateEnabledState ( ) ;
}
private void updateEnabledState ( )
{
if ( linkCompiler ! = null )
linkCompiler . Enabled . Value = UserPlayingState . Value ! = LocalUserPlayingState . Playing ;
}
}
2025-01-14 22:03:37 +08:00
public bool UsesFixedAnchor { get ; set ; }
}
2025-01-14 20:24:31 +08:00
}