diff --git a/.idea/.idea.osu.Server.Spectator/.idea/projectSettingsUpdater.xml b/.idea/.idea.osu.Server.Spectator/.idea/projectSettingsUpdater.xml
index 86cc6c63..64af657f 100644
--- a/.idea/.idea.osu.Server.Spectator/.idea/projectSettingsUpdater.xml
+++ b/.idea/.idea.osu.Server.Spectator/.idea/projectSettingsUpdater.xml
@@ -1,6 +1,7 @@
+
\ No newline at end of file
diff --git a/SampleMultiplayerClient/SampleMultiplayerClient.csproj b/SampleMultiplayerClient/SampleMultiplayerClient.csproj
index 44cad969..3faf8c79 100644
--- a/SampleMultiplayerClient/SampleMultiplayerClient.csproj
+++ b/SampleMultiplayerClient/SampleMultiplayerClient.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/SampleSpectatorClient/SampleSpectatorClient.csproj b/SampleSpectatorClient/SampleSpectatorClient.csproj
index 44cad969..3faf8c79 100644
--- a/SampleSpectatorClient/SampleSpectatorClient.csproj
+++ b/SampleSpectatorClient/SampleSpectatorClient.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/SampleSpectatorClient/SpectatorClient.cs b/SampleSpectatorClient/SpectatorClient.cs
index 0cf2f636..53efe869 100644
--- a/SampleSpectatorClient/SpectatorClient.cs
+++ b/SampleSpectatorClient/SpectatorClient.cs
@@ -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);
diff --git a/osu.Server.Spectator.Tests/SpectatorHubTest.cs b/osu.Server.Spectator.Tests/SpectatorHubTest.cs
index 1893f184..8dc5aca2 100644
--- a/osu.Server.Spectator.Tests/SpectatorHubTest.cs
+++ b/osu.Server.Spectator.Tests/SpectatorHubTest.cs
@@ -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;
@@ -325,9 +324,12 @@ public async Task NewUserBeginsWatchingStream(bool ongoing)
Mock> mockClients = new Mock>();
Mock mockCaller = new Mock();
+ Mock mockStreamer = new Mock();
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 mockGroups = new Mock();
@@ -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(m => m.Equals(state))), Times.Exactly(ongoing ? 2 : 0));
+ mockStreamer.Verify(client => client.UserStartedWatching(It.Is(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]
diff --git a/osu.Server.Spectator/Hubs/Spectator/SpectatorClientState.cs b/osu.Server.Spectator/Hubs/Spectator/SpectatorClientState.cs
index 54aa56c1..2e6b4ea0 100644
--- a/osu.Server.Spectator/Hubs/Spectator/SpectatorClientState.cs
+++ b/osu.Server.Spectator/Hubs/Spectator/SpectatorClientState.cs
@@ -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;
@@ -11,12 +12,26 @@ namespace osu.Server.Spectator.Hubs.Spectator
[Serializable]
public class SpectatorClientState : ClientState
{
+ ///
+ /// When a user is in gameplay, this is the state as conveyed at the start of the play session.
+ ///
public SpectatorState? State;
+ ///
+ /// When a user is in gameplay, this is the imminent score. It will be updated throughout a play session.
+ ///
public Score? Score;
+ ///
+ /// The score token as conveyed by the client at the beginning of a play session.
+ ///
public long? ScoreToken;
+ ///
+ /// The list of IDs of users that this client is currently watching.
+ ///
+ public HashSet WatchedUsers = new HashSet();
+
[JsonConstructor]
public SpectatorClientState(in string connectionId, in int userId)
: base(connectionId, userId)
diff --git a/osu.Server.Spectator/Hubs/Spectator/SpectatorHub.cs b/osu.Server.Spectator/Hubs/Spectator/SpectatorHub.cs
index 43c42b56..892c5e81 100644
--- a/osu.Server.Spectator/Hubs/Spectator/SpectatorHub.cs
+++ b/osu.Server.Spectator/Hubs/Spectator/SpectatorHub.cs
@@ -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;
+ }
}
}
@@ -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()
@@ -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);
}
diff --git a/osu.Server.Spectator/osu.Server.Spectator.csproj b/osu.Server.Spectator/osu.Server.Spectator.csproj
index 2183954a..ab9cee10 100644
--- a/osu.Server.Spectator/osu.Server.Spectator.csproj
+++ b/osu.Server.Spectator/osu.Server.Spectator.csproj
@@ -15,11 +15,11 @@
-
-
-
-
-
+
+
+
+
+