Skip to content

Commit

Permalink
Merge pull request #259 from bdach/who-is-spectating-me
Browse files Browse the repository at this point in the history
Notify users of who is spectating them
  • Loading branch information
peppy authored Jan 23, 2025
2 parents f00eca4 + 19f0e02 commit 239e231
Show file tree
Hide file tree
Showing 8 changed files with 86 additions and 9 deletions.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion SampleMultiplayerClient/SampleMultiplayerClient.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" />
<PackageReference Include="ppy.osu.Game" Version="2025.114.0" />
<PackageReference Include="ppy.osu.Game" Version="2025.122.0" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion SampleSpectatorClient/SampleSpectatorClient.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" />
<PackageReference Include="ppy.osu.Game" Version="2025.114.0" />
<PackageReference Include="ppy.osu.Game" Version="2025.122.0" />
</ItemGroup>

</Project>
13 changes: 13 additions & 0 deletions SampleSpectatorClient/SpectatorClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,19 @@ Task ISpectatorClient.UserScoreProcessed(int userId, long scoreId)
return Task.CompletedTask;
}

Task ISpectatorClient.UserStartedWatching(SpectatorUser[] users)
{
foreach (var user in users)
Console.WriteLine($"{connection.ConnectionId} User {user.OnlineID} started watching you");
return Task.CompletedTask;
}

Task ISpectatorClient.UserEndedWatching(int userId)
{
Console.WriteLine($"{connection.ConnectionId} User {userId} ended watching you");
return Task.CompletedTask;
}

public Task BeginPlaying(long? scoreToken, SpectatorState state) => connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), scoreToken, state);

public Task SendFrames(FrameDataBundle data) => connection.SendAsync(nameof(ISpectatorServer.SendFrameData), data);
Expand Down
10 changes: 9 additions & 1 deletion osu.Server.Spectator.Tests/SpectatorHubTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
using Moq;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Spectator;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Osu.Mods;
Expand Down Expand Up @@ -325,9 +324,12 @@ public async Task NewUserBeginsWatchingStream(bool ongoing)

Mock<IHubCallerClients<ISpectatorClient>> mockClients = new Mock<IHubCallerClients<ISpectatorClient>>();
Mock<ISpectatorClient> mockCaller = new Mock<ISpectatorClient>();
Mock<ISpectatorClient> mockStreamer = new Mock<ISpectatorClient>();

mockClients.Setup(clients => clients.Caller).Returns(mockCaller.Object);
mockClients.Setup(clients => clients.All).Returns(mockCaller.Object);
mockClients.Setup(clients => clients.User(streamer_id.ToString())).Returns(mockStreamer.Object);
mockDatabase.Setup(db => db.GetUsernameAsync(watcher_id)).ReturnsAsync("watcher");

Mock<IGroupManager> mockGroups = new Mock<IGroupManager>();

Expand Down Expand Up @@ -362,6 +364,12 @@ public async Task NewUserBeginsWatchingStream(bool ongoing)
mockGroups.Verify(groups => groups.AddToGroupAsync(connectionId, SpectatorHub.GetGroupId(streamer_id), default));

mockCaller.Verify(clients => clients.UserBeganPlaying(streamer_id, It.Is<SpectatorState>(m => m.Equals(state))), Times.Exactly(ongoing ? 2 : 0));
mockStreamer.Verify(client => client.UserStartedWatching(It.Is<SpectatorUser[]>(users => users.Single().OnlineID == watcher_id)), Times.Once);

await hub.EndWatchingUser(streamer_id);

mockGroups.Verify(groups => groups.RemoveFromGroupAsync(connectionId, SpectatorHub.GetGroupId(streamer_id), default));
mockStreamer.Verify(client => client.UserEndedWatching(watcher_id), Times.Once);
}

[Fact]
Expand Down
15 changes: 15 additions & 0 deletions osu.Server.Spectator/Hubs/Spectator/SpectatorClientState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using osu.Game.Online.Spectator;
using osu.Game.Scoring;
Expand All @@ -11,12 +12,26 @@ namespace osu.Server.Spectator.Hubs.Spectator
[Serializable]
public class SpectatorClientState : ClientState
{
/// <summary>
/// When a user is in gameplay, this is the state as conveyed at the start of the play session.
/// </summary>
public SpectatorState? State;

/// <summary>
/// When a user is in gameplay, this is the imminent score. It will be updated throughout a play session.
/// </summary>
public Score? Score;

/// <summary>
/// The score token as conveyed by the client at the beginning of a play session.
/// </summary>
public long? ScoreToken;

/// <summary>
/// The list of IDs of users that this client is currently watching.
/// </summary>
public HashSet<int> WatchedUsers = new HashSet<int>();

[JsonConstructor]
public SpectatorClientState(in string connectionId, in int userId)
: base(connectionId, userId)
Expand Down
42 changes: 41 additions & 1 deletion osu.Server.Spectator/Hubs/Spectator/SpectatorHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,12 @@ public async Task EndPlaySession(SpectatorState state)
}
finally
{
usage.Destroy();
if (usage.Item != null)
{
usage.Item.State = null;
usage.Item.Score = null;
usage.Item.ScoreToken = null;
}
}
}

Expand Down Expand Up @@ -202,12 +207,44 @@ public async Task StartWatchingUser(int userId)
// user isn't tracked.
}

using (var state = await GetOrCreateLocalUserState())
{
var clientState = state.Item ??= new SpectatorClientState(Context.ConnectionId, Context.GetUserId());
clientState.WatchedUsers.Add(userId);
}

await Groups.AddToGroupAsync(Context.ConnectionId, GetGroupId(userId));

int watcherId = Context.GetUserId();
string? watcherUsername;
using (var db = databaseFactory.GetInstance())
watcherUsername = await db.GetUsernameAsync(watcherId);

if (watcherUsername == null)
return;

var watcher = new SpectatorUser
{
OnlineID = watcherId,
Username = watcherUsername,
};

await Clients.User(userId.ToString()).UserStartedWatching([watcher]);
}

public async Task EndWatchingUser(int userId)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, GetGroupId(userId));

using (var state = await GetOrCreateLocalUserState())
{
var clientState = state.Item ??= new SpectatorClientState(Context.ConnectionId, Context.GetUserId());
clientState.WatchedUsers.Remove(userId);
}

int watcherId = Context.GetUserId();

await Clients.User(userId.ToString()).UserEndedWatching(watcherId);
}

public override async Task OnConnectedAsync()
Expand All @@ -225,6 +262,9 @@ protected override async Task CleanUpState(SpectatorClientState state)
if (state.State != null)
await endPlaySession(state.UserId, state.State);

foreach (int watchedUserId in state.WatchedUsers)
await Clients.User(watchedUserId.ToString()).UserEndedWatching(state.UserId);

await base.CleanUpState(state);
}

Expand Down
10 changes: 5 additions & 5 deletions osu.Server.Spectator/osu.Server.Spectator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.1" />
<PackageReference Include="ppy.osu.Game" Version="2025.114.0" />
<PackageReference Include="ppy.osu.Game.Rulesets.Catch" Version="2025.114.0" />
<PackageReference Include="ppy.osu.Game.Rulesets.Mania" Version="2025.114.0" />
<PackageReference Include="ppy.osu.Game.Rulesets.Osu" Version="2025.114.0" />
<PackageReference Include="ppy.osu.Game.Rulesets.Taiko" Version="2025.114.0" />
<PackageReference Include="ppy.osu.Game" Version="2025.122.0" />
<PackageReference Include="ppy.osu.Game.Rulesets.Catch" Version="2025.122.0" />
<PackageReference Include="ppy.osu.Game.Rulesets.Mania" Version="2025.122.0" />
<PackageReference Include="ppy.osu.Game.Rulesets.Osu" Version="2025.122.0" />
<PackageReference Include="ppy.osu.Game.Rulesets.Taiko" Version="2025.122.0" />
<PackageReference Include="ppy.osu.Server.OsuQueueProcessor" Version="2024.507.0" />
<PackageReference Include="Sentry.AspNetCore" Version="5.0.1" />
</ItemGroup>
Expand Down

0 comments on commit 239e231

Please sign in to comment.