Skip to content

Commit 8f82462

Browse files
authored
Merge pull request #31527 from bdach/spectator-list-ready
Show spectating users during gameplay
2 parents 704c2ea + 0265a29 commit 8f82462

File tree

17 files changed

+267
-42
lines changed

17 files changed

+267
-42
lines changed

osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs

+10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Linq;
55
using osu.Framework.Bindables;
66
using osu.Framework.Graphics;
7+
using osu.Game.Screens.Play.HUD;
78
using osu.Game.Skinning;
89
using osuTK;
910
using osuTK.Graphics;
@@ -47,6 +48,7 @@ public CatchLegacySkinTransformer(ISkin skin)
4748
return new DefaultSkinComponentsContainer(container =>
4849
{
4950
var keyCounter = container.OfType<LegacyKeyCounterDisplay>().FirstOrDefault();
51+
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
5052

5153
if (keyCounter != null)
5254
{
@@ -55,11 +57,19 @@ public CatchLegacySkinTransformer(ISkin skin)
5557
keyCounter.Origin = Anchor.TopRight;
5658
keyCounter.Position = new Vector2(0, -40) * 1.6f;
5759
}
60+
61+
if (spectatorList != null)
62+
{
63+
spectatorList.Anchor = Anchor.BottomLeft;
64+
spectatorList.Origin = Anchor.BottomLeft;
65+
spectatorList.Position = new Vector2(10, -10);
66+
}
5867
})
5968
{
6069
Children = new Drawable[]
6170
{
6271
new LegacyKeyCounterDisplay(),
72+
new SpectatorList(),
6373
}
6474
};
6575
}

osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs

+11
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
using osu.Game.Beatmaps;
1010
using osu.Game.Rulesets.Mania.Beatmaps;
1111
using osu.Game.Rulesets.Scoring;
12+
using osu.Game.Screens.Play.HUD;
1213
using osu.Game.Skinning;
14+
using osuTK;
1315
using osuTK.Graphics;
1416

1517
namespace osu.Game.Rulesets.Mania.Skinning.Argon
@@ -39,6 +41,7 @@ public ManiaArgonSkinTransformer(ISkin skin, IBeatmap beatmap)
3941
return new DefaultSkinComponentsContainer(container =>
4042
{
4143
var combo = container.ChildrenOfType<ArgonManiaComboCounter>().FirstOrDefault();
44+
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
4245

4346
if (combo != null)
4447
{
@@ -47,9 +50,17 @@ public ManiaArgonSkinTransformer(ISkin skin, IBeatmap beatmap)
4750
combo.Origin = Anchor.Centre;
4851
combo.Y = 200;
4952
}
53+
54+
if (spectatorList != null)
55+
spectatorList.Position = new Vector2(36, -66);
5056
})
5157
{
5258
new ArgonManiaComboCounter(),
59+
new SpectatorList
60+
{
61+
Anchor = Anchor.BottomLeft,
62+
Origin = Anchor.BottomLeft,
63+
}
5364
};
5465
}
5566

osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs

+11
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
using osu.Game.Rulesets.Mania.Beatmaps;
1616
using osu.Game.Rulesets.Objects.Legacy;
1717
using osu.Game.Rulesets.Scoring;
18+
using osu.Game.Screens.Play.HUD;
1819
using osu.Game.Skinning;
20+
using osuTK;
1921

2022
namespace osu.Game.Rulesets.Mania.Skinning.Legacy
2123
{
@@ -95,16 +97,25 @@ public override Drawable GetDrawableComponent(ISkinComponentLookup lookup)
9597
return new DefaultSkinComponentsContainer(container =>
9698
{
9799
var combo = container.ChildrenOfType<LegacyManiaComboCounter>().FirstOrDefault();
100+
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
98101

99102
if (combo != null)
100103
{
101104
combo.Anchor = Anchor.TopCentre;
102105
combo.Origin = Anchor.Centre;
103106
combo.Y = this.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ComboPosition)?.Value ?? 0;
104107
}
108+
109+
if (spectatorList != null)
110+
{
111+
spectatorList.Anchor = Anchor.BottomLeft;
112+
spectatorList.Origin = Anchor.BottomLeft;
113+
spectatorList.Position = new Vector2(10, -10);
114+
}
105115
})
106116
{
107117
new LegacyManiaComboCounter(),
118+
new SpectatorList(),
108119
};
109120
}
110121

osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs

+14
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using osu.Framework.Bindables;
77
using osu.Framework.Graphics;
88
using osu.Game.Rulesets.Osu.Objects;
9+
using osu.Game.Screens.Play.HUD;
910
using osu.Game.Skinning;
1011
using osuTK;
1112

@@ -70,19 +71,32 @@ public OsuLegacySkinTransformer(ISkin skin)
7071
}
7172

7273
var combo = container.OfType<LegacyDefaultComboCounter>().FirstOrDefault();
74+
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
75+
76+
Vector2 pos = new Vector2();
7377

7478
if (combo != null)
7579
{
7680
combo.Anchor = Anchor.BottomLeft;
7781
combo.Origin = Anchor.BottomLeft;
7882
combo.Scale = new Vector2(1.28f);
83+
84+
pos += new Vector2(10, -(combo.DrawHeight * 1.56f + 20) * combo.Scale.X);
85+
}
86+
87+
if (spectatorList != null)
88+
{
89+
spectatorList.Anchor = Anchor.BottomLeft;
90+
spectatorList.Origin = Anchor.BottomLeft;
91+
spectatorList.Position = pos;
7992
}
8093
})
8194
{
8295
Children = new Drawable[]
8396
{
8497
new LegacyDefaultComboCounter(),
8598
new LegacyKeyCounterDisplay(),
99+
new SpectatorList(),
86100
}
87101
};
88102
}
Binary file not shown.

osu.Game.Tests/Skins/SkinDeserialisationTest.cs

+2
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ public class SkinDeserialisationTest
7171
"Archives/modified-classic-20240724.osk",
7272
// Covers skinnable mod display
7373
"Archives/modified-default-20241207.osk",
74+
// Covers skinnable spectator list
75+
"Archives/modified-argon-20250116.osk",
7476
};
7577

7678
/// <summary>

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

+39-13
Original file line numberDiff line numberDiff line change
@@ -6,48 +6,74 @@
66
using osu.Framework.Bindables;
77
using osu.Framework.Graphics;
88
using osu.Framework.Utils;
9+
using osu.Game.Beatmaps;
910
using osu.Game.Graphics;
11+
using osu.Game.Online.Spectator;
12+
using osu.Game.Rulesets.Osu;
13+
using osu.Game.Rulesets.Osu.Scoring;
1014
using osu.Game.Screens.Play;
1115
using osu.Game.Screens.Play.HUD;
16+
using osu.Game.Tests.Visual.Spectator;
1217

1318
namespace osu.Game.Tests.Visual.Gameplay
1419
{
1520
[TestFixture]
1621
public partial class TestSceneSpectatorList : OsuTestScene
1722
{
18-
private readonly BindableList<SpectatorList.Spectator> spectators = new BindableList<SpectatorList.Spectator>();
19-
private readonly Bindable<LocalUserPlayingState> localUserPlayingState = new Bindable<LocalUserPlayingState>();
20-
2123
private int counter;
2224

2325
[Test]
2426
public void TestBasics()
2527
{
2628
SpectatorList list = null!;
27-
AddStep("create spectator list", () => Child = list = new SpectatorList
29+
Bindable<LocalUserPlayingState> playingState = new Bindable<LocalUserPlayingState>();
30+
GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), healthProcessor: new OsuHealthProcessor(0), localUserPlayingState: playingState);
31+
TestSpectatorClient client = new TestSpectatorClient();
32+
33+
AddStep("create spectator list", () =>
2834
{
29-
Anchor = Anchor.Centre,
30-
Origin = Anchor.Centre,
31-
Spectators = { BindTarget = spectators },
32-
UserPlayingState = { BindTarget = localUserPlayingState }
35+
Children = new Drawable[]
36+
{
37+
client,
38+
new DependencyProvidingContainer
39+
{
40+
RelativeSizeAxes = Axes.Both,
41+
CachedDependencies =
42+
[
43+
(typeof(GameplayState), gameplayState),
44+
(typeof(SpectatorClient), client)
45+
],
46+
Child = list = new SpectatorList
47+
{
48+
Anchor = Anchor.Centre,
49+
Origin = Anchor.Centre,
50+
}
51+
}
52+
};
3353
});
3454

35-
AddStep("start playing", () => localUserPlayingState.Value = LocalUserPlayingState.Playing);
55+
AddStep("start playing", () => playingState.Value = LocalUserPlayingState.Playing);
3656

3757
AddRepeatStep("add a user", () =>
3858
{
3959
int id = Interlocked.Increment(ref counter);
40-
spectators.Add(new SpectatorList.Spectator(id, $"User {id}"));
60+
((ISpectatorClient)client).UserStartedWatching([
61+
new SpectatorUser
62+
{
63+
OnlineID = id,
64+
Username = $"User {id}"
65+
}
66+
]);
4167
}, 10);
4268

43-
AddRepeatStep("remove random user", () => spectators.RemoveAt(RNG.Next(0, spectators.Count)), 5);
69+
AddRepeatStep("remove random user", () => ((ISpectatorClient)client).UserEndedWatching(client.WatchingUsers[RNG.Next(client.WatchingUsers.Count)].OnlineID), 5);
4470

4571
AddStep("change font to venera", () => list.Font.Value = Typeface.Venera);
4672
AddStep("change font to torus", () => list.Font.Value = Typeface.Torus);
4773
AddStep("change header colour", () => list.HeaderColour.Value = new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1));
4874

49-
AddStep("enter break", () => localUserPlayingState.Value = LocalUserPlayingState.Break);
50-
AddStep("stop playing", () => localUserPlayingState.Value = LocalUserPlayingState.NotPlaying);
75+
AddStep("enter break", () => playingState.Value = LocalUserPlayingState.Break);
76+
AddStep("stop playing", () => playingState.Value = LocalUserPlayingState.NotPlaying);
5177
}
5278
}
5379
}

osu.Game/Online/Spectator/ISpectatorClient.cs

+12
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,17 @@ public interface ISpectatorClient : IStatefulUserHubClient
3737
/// <param name="userId">The ID of the user who achieved the score.</param>
3838
/// <param name="scoreId">The ID of the score.</param>
3939
Task UserScoreProcessed(int userId, long scoreId);
40+
41+
/// <summary>
42+
/// Signals that another user has <see cref="ISpectatorServer.StartWatchingUser">started watching this client</see>.
43+
/// </summary>
44+
/// <param name="user">The information about the user who started watching.</param>
45+
Task UserStartedWatching(SpectatorUser[] user);
46+
47+
/// <summary>
48+
/// Signals that another user has <see cref="ISpectatorServer.EndWatchingUser">ended watching this client</see>
49+
/// </summary>
50+
/// <param name="userId">The ID of the user who ended watching.</param>
51+
Task UserEndedWatching(int userId);
4052
}
4153
}

osu.Game/Online/Spectator/OnlineSpectatorClient.cs

+2
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ private void load(IAPIProvider api)
4242
connection.On<int, FrameDataBundle>(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames);
4343
connection.On<int, SpectatorState>(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying);
4444
connection.On<int, long>(nameof(ISpectatorClient.UserScoreProcessed), ((ISpectatorClient)this).UserScoreProcessed);
45+
connection.On<SpectatorUser[]>(nameof(ISpectatorClient.UserStartedWatching), ((ISpectatorClient)this).UserStartedWatching);
46+
connection.On<int>(nameof(ISpectatorClient.UserEndedWatching), ((ISpectatorClient)this).UserEndedWatching);
4547
connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IStatefulUserHubClient)this).DisconnectRequested);
4648
};
4749

osu.Game/Online/Spectator/SpectatorClient.cs

+35-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Diagnostics;
77
using System.Linq;
88
using System.Threading.Tasks;
9+
using JetBrains.Annotations;
910
using osu.Framework.Allocation;
1011
using osu.Framework.Bindables;
1112
using osu.Framework.Development;
@@ -36,10 +37,16 @@ public abstract partial class SpectatorClient : Component, ISpectatorClient
3637
public abstract IBindable<bool> IsConnected { get; }
3738

3839
/// <summary>
39-
/// The states of all users currently being watched.
40+
/// The states of all users currently being watched by the local user.
4041
/// </summary>
42+
[UsedImplicitly] // Marked virtual due to mock use in testing
4143
public virtual IBindableDictionary<int, SpectatorState> WatchedUserStates => watchedUserStates;
4244

45+
/// <summary>
46+
/// All users who are currently watching the local user.
47+
/// </summary>
48+
public IBindableList<SpectatorUser> WatchingUsers => watchingUsers;
49+
4350
/// <summary>
4451
/// A global list of all players currently playing.
4552
/// </summary>
@@ -53,6 +60,7 @@ public abstract partial class SpectatorClient : Component, ISpectatorClient
5360
/// <summary>
5461
/// Called whenever new frames arrive from the server.
5562
/// </summary>
63+
[UsedImplicitly] // Marked virtual due to mock use in testing
5664
public virtual event Action<int, FrameDataBundle>? OnNewFrames;
5765

5866
/// <summary>
@@ -82,6 +90,7 @@ public abstract partial class SpectatorClient : Component, ISpectatorClient
8290

8391
private readonly BindableDictionary<int, SpectatorState> watchedUserStates = new BindableDictionary<int, SpectatorState>();
8492

93+
private readonly BindableList<SpectatorUser> watchingUsers = new BindableList<SpectatorUser>();
8594
private readonly BindableList<int> playingUsers = new BindableList<int>();
8695
private readonly SpectatorState currentState = new SpectatorState();
8796

@@ -127,6 +136,7 @@ private void load()
127136
{
128137
playingUsers.Clear();
129138
watchedUserStates.Clear();
139+
watchingUsers.Clear();
130140
}
131141
}), true);
132142
}
@@ -179,6 +189,30 @@ Task ISpectatorClient.UserScoreProcessed(int userId, long scoreId)
179189
return Task.CompletedTask;
180190
}
181191

192+
Task ISpectatorClient.UserStartedWatching(SpectatorUser[] users)
193+
{
194+
Schedule(() =>
195+
{
196+
foreach (var user in users)
197+
{
198+
if (!watchingUsers.Contains(user))
199+
watchingUsers.Add(user);
200+
}
201+
});
202+
203+
return Task.CompletedTask;
204+
}
205+
206+
Task ISpectatorClient.UserEndedWatching(int userId)
207+
{
208+
Schedule(() =>
209+
{
210+
watchingUsers.RemoveAll(u => u.OnlineID == userId);
211+
});
212+
213+
return Task.CompletedTask;
214+
}
215+
182216
Task IStatefulUserHubClient.DisconnectRequested()
183217
{
184218
Schedule(() => DisconnectInternal());
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2+
// See the LICENCE file in the repository root for full licence text.
3+
4+
using System;
5+
using MessagePack;
6+
using osu.Game.Users;
7+
8+
namespace osu.Game.Online.Spectator
9+
{
10+
[Serializable]
11+
[MessagePackObject]
12+
public class SpectatorUser : IUser, IEquatable<SpectatorUser>
13+
{
14+
[Key(0)]
15+
public int OnlineID { get; set; }
16+
17+
[Key(1)]
18+
public string Username { get; set; } = string.Empty;
19+
20+
[IgnoreMember]
21+
public CountryCode CountryCode => CountryCode.Unknown;
22+
23+
[IgnoreMember]
24+
public bool IsBot => false;
25+
26+
public bool Equals(SpectatorUser? other)
27+
{
28+
if (other is null) return false;
29+
if (ReferenceEquals(this, other)) return true;
30+
31+
return OnlineID == other.OnlineID;
32+
}
33+
34+
public override bool Equals(object? obj) => Equals(obj as SpectatorUser);
35+
36+
// ReSharper disable once NonReadonlyMemberInGetHashCode
37+
public override int GetHashCode() => OnlineID;
38+
}
39+
}

0 commit comments

Comments
 (0)