Skip to content

Commit 8dc6167

Browse files
authored
Merge pull request ppy#31844 from bdach/fix-mp-players-appearing-as-spectators
Fix spectator list showing other users in multiplayer room even if they're not spectating
2 parents 6ff1a13 + 310700b commit 8dc6167

File tree

2 files changed

+54
-23
lines changed

2 files changed

+54
-23
lines changed

osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@
88
using osu.Framework.Utils;
99
using osu.Game.Beatmaps;
1010
using osu.Game.Graphics;
11+
using osu.Game.Online.Multiplayer;
1112
using osu.Game.Online.Spectator;
1213
using osu.Game.Rulesets.Osu;
1314
using osu.Game.Rulesets.Osu.Scoring;
1415
using osu.Game.Screens.Play;
1516
using osu.Game.Screens.Play.HUD;
17+
using osu.Game.Tests.Visual.Multiplayer;
18+
using osu.Game.Tests.Visual.OnlinePlay;
1619
using osu.Game.Tests.Visual.Spectator;
1720

1821
namespace osu.Game.Tests.Visual.Gameplay
@@ -28,20 +31,23 @@ public void TestBasics()
2831
SpectatorList list = null!;
2932
Bindable<LocalUserPlayingState> playingState = new Bindable<LocalUserPlayingState>();
3033
GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), healthProcessor: new OsuHealthProcessor(0), localUserPlayingState: playingState);
31-
TestSpectatorClient client = new TestSpectatorClient();
34+
TestSpectatorClient spectatorClient = new TestSpectatorClient();
35+
TestMultiplayerClient multiplayerClient = new TestMultiplayerClient(new TestMultiplayerRoomManager(new TestRoomRequestsHandler()));
3236

3337
AddStep("create spectator list", () =>
3438
{
3539
Children = new Drawable[]
3640
{
37-
client,
41+
spectatorClient,
42+
multiplayerClient,
3843
new DependencyProvidingContainer
3944
{
4045
RelativeSizeAxes = Axes.Both,
4146
CachedDependencies =
4247
[
4348
(typeof(GameplayState), gameplayState),
44-
(typeof(SpectatorClient), client)
49+
(typeof(SpectatorClient), spectatorClient),
50+
(typeof(MultiplayerClient), multiplayerClient),
4551
],
4652
Child = list = new SpectatorList
4753
{
@@ -57,7 +63,7 @@ public void TestBasics()
5763
AddRepeatStep("add a user", () =>
5864
{
5965
int id = Interlocked.Increment(ref counter);
60-
((ISpectatorClient)client).UserStartedWatching([
66+
((ISpectatorClient)spectatorClient).UserStartedWatching([
6167
new SpectatorUser
6268
{
6369
OnlineID = id,
@@ -66,7 +72,8 @@ public void TestBasics()
6672
]);
6773
}, 10);
6874

69-
AddRepeatStep("remove random user", () => ((ISpectatorClient)client).UserEndedWatching(client.WatchingUsers[RNG.Next(client.WatchingUsers.Count)].OnlineID), 5);
75+
AddRepeatStep("remove random user", () => ((ISpectatorClient)spectatorClient).UserEndedWatching(
76+
spectatorClient.WatchingUsers[RNG.Next(spectatorClient.WatchingUsers.Count)].OnlineID), 5);
7077

7178
AddStep("change font to venera", () => list.Font.Value = Typeface.Venera);
7279
AddStep("change font to torus", () => list.Font.Value = Typeface.Torus);

osu.Game/Screens/Play/HUD/SpectatorList.cs

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
// See the LICENCE file in the repository root for full licence text.
33

44
using System;
5+
using System.Collections.Generic;
56
using System.Collections.Specialized;
7+
using System.Linq;
68
using osu.Framework.Allocation;
79
using osu.Framework.Bindables;
810
using osu.Framework.Extensions.Color4Extensions;
@@ -17,6 +19,7 @@
1719
using osu.Game.Online.Chat;
1820
using osu.Game.Localisation.HUD;
1921
using osu.Game.Localisation.SkinComponents;
22+
using osu.Game.Online.Multiplayer;
2023
using osu.Game.Online.Spectator;
2124
using osu.Game.Skinning;
2225
using 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

Comments
 (0)