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 @@ - - - - - + + + + +