Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Notify users of who is spectating them #259

Merged
merged 7 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

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