1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-31 00:02:55 +08:00

Merge pull request #12881 from smoogipoo/restructure-spectator-client

Restructure and rename spectator client classes
This commit is contained in:
Dean Herbert 2021-05-20 19:53:57 +09:00 committed by GitHub
commit 46f5498935
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 341 additions and 264 deletions

View File

@ -27,8 +27,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
private readonly User streamingUser = new User { Id = MultiplayerTestScene.PLAYER_1_ID, Username = "Test user" }; private readonly User streamingUser = new User { Id = MultiplayerTestScene.PLAYER_1_ID, Username = "Test user" };
[Cached(typeof(SpectatorStreamingClient))] [Cached(typeof(SpectatorClient))]
private TestSpectatorStreamingClient testSpectatorStreamingClient = new TestSpectatorStreamingClient(); private TestSpectatorClient testSpectatorClient = new TestSpectatorClient();
[Cached(typeof(UserLookupCache))] [Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestUserLookupCache(); private UserLookupCache lookupCache = new TestUserLookupCache();
@ -61,8 +61,8 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("add streaming client", () => AddStep("add streaming client", () =>
{ {
Remove(testSpectatorStreamingClient); Remove(testSpectatorClient);
Add(testSpectatorStreamingClient); Add(testSpectatorClient);
}); });
finish(); finish();
@ -212,9 +212,9 @@ namespace osu.Game.Tests.Visual.Gameplay
private void waitForPlayer() => AddUntilStep("wait for player", () => Stack.CurrentScreen is Player); private void waitForPlayer() => AddUntilStep("wait for player", () => Stack.CurrentScreen is Player);
private void start(int? beatmapId = null) => AddStep("start play", () => testSpectatorStreamingClient.StartPlay(streamingUser.Id, beatmapId ?? importedBeatmapId)); private void start(int? beatmapId = null) => AddStep("start play", () => testSpectatorClient.StartPlay(streamingUser.Id, beatmapId ?? importedBeatmapId));
private void finish(int? beatmapId = null) => AddStep("end play", () => testSpectatorStreamingClient.EndPlay(streamingUser.Id, beatmapId ?? importedBeatmapId)); private void finish() => AddStep("end play", () => testSpectatorClient.EndPlay(streamingUser.Id));
private void checkPaused(bool state) => private void checkPaused(bool state) =>
AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType<DrawableRuleset>().First().IsPaused.Value == state); AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType<DrawableRuleset>().First().IsPaused.Value == state);
@ -223,7 +223,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
AddStep("send frames", () => AddStep("send frames", () =>
{ {
testSpectatorStreamingClient.SendFrames(streamingUser.Id, nextFrame, count); testSpectatorClient.SendFrames(streamingUser.Id, nextFrame, count);
nextFrame += count; nextFrame += count;
}); });
} }

View File

@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private IAPIProvider api { get; set; } private IAPIProvider api { get; set; }
[Resolved] [Resolved]
private SpectatorStreamingClient streamingClient { get; set; } private SpectatorClient spectatorClient { get; set; }
[Cached] [Cached]
private GameplayBeatmap gameplayBeatmap = new GameplayBeatmap(new Beatmap()); private GameplayBeatmap gameplayBeatmap = new GameplayBeatmap(new Beatmap());
@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
replay = new Replay(); replay = new Replay();
users.BindTo(streamingClient.PlayingUsers); users.BindTo(spectatorClient.PlayingUsers);
users.BindCollectionChanged((obj, args) => users.BindCollectionChanged((obj, args) =>
{ {
switch (args.Action) switch (args.Action)
@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.Gameplay
foreach (int user in args.NewItems) foreach (int user in args.NewItems)
{ {
if (user == api.LocalUser.Value.Id) if (user == api.LocalUser.Value.Id)
streamingClient.WatchUser(user); spectatorClient.WatchUser(user);
} }
break; break;
@ -91,14 +91,14 @@ namespace osu.Game.Tests.Visual.Gameplay
foreach (int user in args.OldItems) foreach (int user in args.OldItems)
{ {
if (user == api.LocalUser.Value.Id) if (user == api.LocalUser.Value.Id)
streamingClient.StopWatchingUser(user); spectatorClient.StopWatchingUser(user);
} }
break; break;
} }
}, true); }, true);
streamingClient.OnNewFrames += onNewFrames; spectatorClient.OnNewFrames += onNewFrames;
Add(new GridContainer Add(new GridContainer
{ {
@ -189,7 +189,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
} }
private double latency = SpectatorStreamingClient.TIME_BETWEEN_SENDS; private double latency = SpectatorClient.TIME_BETWEEN_SENDS;
protected override void Update() protected override void Update()
{ {
@ -233,7 +233,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("stop recorder", () => AddStep("stop recorder", () =>
{ {
recorder.Expire(); recorder.Expire();
streamingClient.OnNewFrames -= onNewFrames; spectatorClient.OnNewFrames -= onNewFrames;
}); });
} }

View File

@ -23,8 +23,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
public class TestSceneMultiSpectatorLeaderboard : MultiplayerTestScene public class TestSceneMultiSpectatorLeaderboard : MultiplayerTestScene
{ {
[Cached(typeof(SpectatorStreamingClient))] [Cached(typeof(SpectatorClient))]
private TestSpectatorStreamingClient streamingClient = new TestSpectatorStreamingClient(); private TestSpectatorClient spectatorClient = new TestSpectatorClient();
[Cached(typeof(UserLookupCache))] [Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestUserLookupCache(); private UserLookupCache lookupCache = new TestUserLookupCache();
@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
base.Content.AddRange(new Drawable[] base.Content.AddRange(new Drawable[]
{ {
streamingClient, spectatorClient,
lookupCache, lookupCache,
content = new Container { RelativeSizeAxes = Axes.Both } content = new Container { RelativeSizeAxes = Axes.Both }
}); });
@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
foreach (var (userId, clock) in clocks) foreach (var (userId, clock) in clocks)
{ {
streamingClient.EndPlay(userId, 0); spectatorClient.EndPlay(userId);
clock.CurrentTime = 0; clock.CurrentTime = 0;
} }
}); });
@ -67,7 +67,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("create leaderboard", () => AddStep("create leaderboard", () =>
{ {
foreach (var (userId, _) in clocks) foreach (var (userId, _) in clocks)
streamingClient.StartPlay(userId, 0); spectatorClient.StartPlay(userId, 0);
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
@ -96,10 +96,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
// For player 2, send frames in sets of 10. // For player 2, send frames in sets of 10.
for (int i = 0; i < 100; i++) for (int i = 0; i < 100; i++)
{ {
streamingClient.SendFrames(PLAYER_1_ID, i, 1); spectatorClient.SendFrames(PLAYER_1_ID, i, 1);
if (i % 10 == 0) if (i % 10 == 0)
streamingClient.SendFrames(PLAYER_2_ID, i, 10); spectatorClient.SendFrames(PLAYER_2_ID, i, 10);
} }
}); });

View File

@ -22,8 +22,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
public class TestSceneMultiSpectatorScreen : MultiplayerTestScene public class TestSceneMultiSpectatorScreen : MultiplayerTestScene
{ {
[Cached(typeof(SpectatorStreamingClient))] [Cached(typeof(SpectatorClient))]
private TestSpectatorStreamingClient streamingClient = new TestSpectatorStreamingClient(); private TestSpectatorClient spectatorClient = new TestSpectatorClient();
[Cached(typeof(UserLookupCache))] [Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestUserLookupCache(); private UserLookupCache lookupCache = new TestUserLookupCache();
@ -59,14 +59,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("add streaming client", () => AddStep("add streaming client", () =>
{ {
Remove(streamingClient); Remove(spectatorClient);
Add(streamingClient); Add(spectatorClient);
}); });
AddStep("finish previous gameplay", () => AddStep("finish previous gameplay", () =>
{ {
foreach (var id in playingUserIds) foreach (var id in playingUserIds)
streamingClient.EndPlay(id, importedBeatmapId); spectatorClient.EndPlay(id);
playingUserIds.Clear(); playingUserIds.Clear();
}); });
} }
@ -87,11 +87,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
loadSpectateScreen(false); loadSpectateScreen(false);
AddWaitStep("wait a bit", 10); AddWaitStep("wait a bit", 10);
AddStep("load player first_player_id", () => streamingClient.StartPlay(PLAYER_1_ID, importedBeatmapId)); AddStep("load player first_player_id", () => spectatorClient.StartPlay(PLAYER_1_ID, importedBeatmapId));
AddUntilStep("one player added", () => spectatorScreen.ChildrenOfType<Player>().Count() == 1); AddUntilStep("one player added", () => spectatorScreen.ChildrenOfType<Player>().Count() == 1);
AddWaitStep("wait a bit", 10); AddWaitStep("wait a bit", 10);
AddStep("load player second_player_id", () => streamingClient.StartPlay(PLAYER_2_ID, importedBeatmapId)); AddStep("load player second_player_id", () => spectatorClient.StartPlay(PLAYER_2_ID, importedBeatmapId));
AddUntilStep("two players added", () => spectatorScreen.ChildrenOfType<Player>().Count() == 2); AddUntilStep("two players added", () => spectatorScreen.ChildrenOfType<Player>().Count() == 2);
} }
@ -251,18 +251,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
foreach (int id in userIds) foreach (int id in userIds)
{ {
Client.CurrentMatchPlayingUserIds.Add(id); Client.CurrentMatchPlayingUserIds.Add(id);
streamingClient.StartPlay(id, beatmapId ?? importedBeatmapId); spectatorClient.StartPlay(id, beatmapId ?? importedBeatmapId);
playingUserIds.Add(id); playingUserIds.Add(id);
nextFrame[id] = 0; nextFrame[id] = 0;
} }
}); });
} }
private void finish(int userId, int? beatmapId = null) private void finish(int userId)
{ {
AddStep("end play", () => AddStep("end play", () =>
{ {
streamingClient.EndPlay(userId, beatmapId ?? importedBeatmapId); spectatorClient.EndPlay(userId);
playingUserIds.Remove(userId); playingUserIds.Remove(userId);
nextFrame.Remove(userId); nextFrame.Remove(userId);
}); });
@ -276,7 +276,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
foreach (int id in userIds) foreach (int id in userIds)
{ {
streamingClient.SendFrames(id, nextFrame[id], count); spectatorClient.SendFrames(id, nextFrame[id], count);
nextFrame[id] += count; nextFrame[id] += count;
} }
}); });

View File

@ -28,8 +28,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
private const int users = 16; private const int users = 16;
[Cached(typeof(SpectatorStreamingClient))] [Cached(typeof(SpectatorClient))]
private TestMultiplayerStreaming streamingClient = new TestMultiplayerStreaming(); private TestMultiplayerSpectatorClient spectatorClient = new TestMultiplayerSpectatorClient();
[Cached(typeof(UserLookupCache))] [Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestSceneCurrentlyPlayingDisplay.TestUserLookupCache(); private UserLookupCache lookupCache = new TestSceneCurrentlyPlayingDisplay.TestUserLookupCache();
@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
base.Content.Children = new Drawable[] base.Content.Children = new Drawable[]
{ {
streamingClient, spectatorClient,
lookupCache, lookupCache,
Content Content
}; };
@ -71,10 +71,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
for (int i = 0; i < users; i++) for (int i = 0; i < users; i++)
streamingClient.StartPlay(i, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0); spectatorClient.StartPlay(i, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0);
Client.CurrentMatchPlayingUserIds.Clear(); Client.CurrentMatchPlayingUserIds.Clear();
Client.CurrentMatchPlayingUserIds.AddRange(streamingClient.PlayingUsers); Client.CurrentMatchPlayingUserIds.AddRange(spectatorClient.PlayingUsers);
Children = new Drawable[] Children = new Drawable[]
{ {
@ -83,7 +83,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
scoreProcessor.ApplyBeatmap(playable); scoreProcessor.ApplyBeatmap(playable);
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, streamingClient.PlayingUsers.ToArray()) LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, spectatorClient.PlayingUsers.ToArray())
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -96,7 +96,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test] [Test]
public void TestScoreUpdates() public void TestScoreUpdates()
{ {
AddRepeatStep("update state", () => streamingClient.RandomlyUpdateState(), 100); AddRepeatStep("update state", () => spectatorClient.RandomlyUpdateState(), 100);
AddToggleStep("switch compact mode", expanded => leaderboard.Expanded.Value = expanded); AddToggleStep("switch compact mode", expanded => leaderboard.Expanded.Value = expanded);
} }
@ -109,12 +109,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test] [Test]
public void TestChangeScoringMode() public void TestChangeScoringMode()
{ {
AddRepeatStep("update state", () => streamingClient.RandomlyUpdateState(), 5); AddRepeatStep("update state", () => spectatorClient.RandomlyUpdateState(), 5);
AddStep("change to classic", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Classic)); AddStep("change to classic", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Classic));
AddStep("change to standardised", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised)); AddStep("change to standardised", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised));
} }
public class TestMultiplayerStreaming : TestSpectatorStreamingClient public class TestMultiplayerSpectatorClient : TestSpectatorClient
{ {
private readonly Dictionary<int, FrameHeader> lastHeaders = new Dictionary<int, FrameHeader>(); private readonly Dictionary<int, FrameHeader> lastHeaders = new Dictionary<int, FrameHeader>();

View File

@ -19,8 +19,10 @@ namespace osu.Game.Tests.Visual.Online
{ {
public class TestSceneCurrentlyPlayingDisplay : OsuTestScene public class TestSceneCurrentlyPlayingDisplay : OsuTestScene
{ {
[Cached(typeof(SpectatorStreamingClient))] private readonly User streamingUser = new User { Id = 2, Username = "Test user" };
private TestSpectatorStreamingClient testSpectatorStreamingClient = new TestSpectatorStreamingClient();
[Cached(typeof(SpectatorClient))]
private TestSpectatorClient testSpectatorClient = new TestSpectatorClient();
private CurrentlyPlayingDisplay currentlyPlaying; private CurrentlyPlayingDisplay currentlyPlaying;
@ -34,7 +36,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
AddStep("add streaming client", () => AddStep("add streaming client", () =>
{ {
nestedContainer?.Remove(testSpectatorStreamingClient); nestedContainer?.Remove(testSpectatorClient);
Remove(lookupCache); Remove(lookupCache);
Children = new Drawable[] Children = new Drawable[]
@ -45,7 +47,7 @@ namespace osu.Game.Tests.Visual.Online
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]
{ {
testSpectatorStreamingClient, testSpectatorClient,
currentlyPlaying = new CurrentlyPlayingDisplay currentlyPlaying = new CurrentlyPlayingDisplay
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -55,15 +57,15 @@ namespace osu.Game.Tests.Visual.Online
}; };
}); });
AddStep("Reset players", () => testSpectatorStreamingClient.PlayingUsers.Clear()); AddStep("Reset players", () => testSpectatorClient.EndPlay(streamingUser.Id));
} }
[Test] [Test]
public void TestBasicDisplay() public void TestBasicDisplay()
{ {
AddStep("Add playing user", () => testSpectatorStreamingClient.PlayingUsers.Add(2)); AddStep("Add playing user", () => testSpectatorClient.StartPlay(streamingUser.Id, 0));
AddUntilStep("Panel loaded", () => currentlyPlaying.ChildrenOfType<UserGridPanel>()?.FirstOrDefault()?.User.Id == 2); AddUntilStep("Panel loaded", () => currentlyPlaying.ChildrenOfType<UserGridPanel>()?.FirstOrDefault()?.User.Id == 2);
AddStep("Remove playing user", () => testSpectatorStreamingClient.PlayingUsers.Remove(2)); AddStep("Remove playing user", () => testSpectatorClient.EndPlay(streamingUser.Id));
AddUntilStep("Panel no longer present", () => !currentlyPlaying.ChildrenOfType<UserGridPanel>().Any()); AddUntilStep("Panel no longer present", () => !currentlyPlaying.ChildrenOfType<UserGridPanel>().Any());
} }

View File

@ -0,0 +1,89 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Client;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Online.API;
namespace osu.Game.Online.Spectator
{
public class OnlineSpectatorClient : SpectatorClient
{
private readonly string endpoint;
private IHubClientConnector? connector;
public override IBindable<bool> IsConnected { get; } = new BindableBool();
private HubConnection? connection => connector?.CurrentConnection;
public OnlineSpectatorClient(EndpointConfiguration endpoints)
{
endpoint = endpoints.SpectatorEndpointUrl;
}
[BackgroundDependencyLoader]
private void load(IAPIProvider api)
{
connector = api.GetHubConnector(nameof(SpectatorClient), endpoint);
if (connector != null)
{
connector.ConfigureConnection = connection =>
{
// until strong typed client support is added, each method must be manually bound
// (see https://github.com/dotnet/aspnetcore/issues/15198)
connection.On<int, SpectatorState>(nameof(ISpectatorClient.UserBeganPlaying), ((ISpectatorClient)this).UserBeganPlaying);
connection.On<int, FrameDataBundle>(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames);
connection.On<int, SpectatorState>(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying);
};
IsConnected.BindTo(connector.IsConnected);
}
}
protected override Task BeginPlayingInternal(SpectatorState state)
{
if (!IsConnected.Value)
return Task.CompletedTask;
return connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), state);
}
protected override Task SendFramesInternal(FrameDataBundle data)
{
if (!IsConnected.Value)
return Task.CompletedTask;
return connection.SendAsync(nameof(ISpectatorServer.SendFrameData), data);
}
protected override Task EndPlayingInternal(SpectatorState state)
{
if (!IsConnected.Value)
return Task.CompletedTask;
return connection.SendAsync(nameof(ISpectatorServer.EndPlaySession), state);
}
protected override Task WatchUserInternal(int userId)
{
if (!IsConnected.Value)
return Task.CompletedTask;
return connection.SendAsync(nameof(ISpectatorServer.StartWatchingUser), userId);
}
protected override Task StopWatchingUserInternal(int userId)
{
if (!IsConnected.Value)
return Task.CompletedTask;
return connection.SendAsync(nameof(ISpectatorServer.EndWatchingUser), userId);
}
}
}

View File

@ -1,13 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.AspNetCore.SignalR.Client;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -23,21 +23,18 @@ using osu.Game.Screens.Play;
namespace osu.Game.Online.Spectator namespace osu.Game.Online.Spectator
{ {
public class SpectatorStreamingClient : Component, ISpectatorClient public abstract class SpectatorClient : Component, ISpectatorClient
{ {
/// <summary> /// <summary>
/// The maximum milliseconds between frame bundle sends. /// The maximum milliseconds between frame bundle sends.
/// </summary> /// </summary>
public const double TIME_BETWEEN_SENDS = 200; public const double TIME_BETWEEN_SENDS = 200;
private readonly string endpoint; /// <summary>
/// Whether the <see cref="SpectatorClient"/> is currently connected.
[CanBeNull] /// This is NOT thread safe and usage should be scheduled.
private IHubClientConnector connector; /// </summary>
public abstract IBindable<bool> IsConnected { get; }
private readonly IBindable<bool> isConnected = new BindableBool();
private HubConnection connection => connector?.CurrentConnection;
private readonly List<int> watchingUsers = new List<int>(); private readonly List<int> watchingUsers = new List<int>();
@ -49,60 +46,42 @@ namespace osu.Game.Online.Spectator
private readonly Dictionary<int, SpectatorState> playingUserStates = new Dictionary<int, SpectatorState>(); private readonly Dictionary<int, SpectatorState> playingUserStates = new Dictionary<int, SpectatorState>();
[CanBeNull] private IBeatmap? currentBeatmap;
private IBeatmap currentBeatmap;
[CanBeNull] private Score? currentScore;
private Score currentScore;
[Resolved] [Resolved]
private IBindable<RulesetInfo> currentRuleset { get; set; } private IBindable<RulesetInfo> currentRuleset { get; set; } = null!;
[Resolved] [Resolved]
private IBindable<IReadOnlyList<Mod>> currentMods { get; set; } private IBindable<IReadOnlyList<Mod>> currentMods { get; set; } = null!;
private readonly SpectatorState currentState = new SpectatorState(); private readonly SpectatorState currentState = new SpectatorState();
private bool isPlaying; /// <summary>
/// Whether the local user is playing.
/// </summary>
protected bool IsPlaying { get; private set; }
/// <summary> /// <summary>
/// Called whenever new frames arrive from the server. /// Called whenever new frames arrive from the server.
/// </summary> /// </summary>
public event Action<int, FrameDataBundle> OnNewFrames; public event Action<int, FrameDataBundle>? OnNewFrames;
/// <summary> /// <summary>
/// Called whenever a user starts a play session, or immediately if the user is being watched and currently in a play session. /// Called whenever a user starts a play session, or immediately if the user is being watched and currently in a play session.
/// </summary> /// </summary>
public event Action<int, SpectatorState> OnUserBeganPlaying; public event Action<int, SpectatorState>? OnUserBeganPlaying;
/// <summary> /// <summary>
/// Called whenever a user finishes a play session. /// Called whenever a user finishes a play session.
/// </summary> /// </summary>
public event Action<int, SpectatorState> OnUserFinishedPlaying; public event Action<int, SpectatorState>? OnUserFinishedPlaying;
public SpectatorStreamingClient(EndpointConfiguration endpoints)
{
endpoint = endpoints.SpectatorEndpointUrl;
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(IAPIProvider api) private void load()
{ {
connector = api.GetHubConnector(nameof(SpectatorStreamingClient), endpoint); IsConnected.BindValueChanged(connected =>
if (connector != null)
{
connector.ConfigureConnection = connection =>
{
// until strong typed client support is added, each method must be manually bound
// (see https://github.com/dotnet/aspnetcore/issues/15198)
connection.On<int, SpectatorState>(nameof(ISpectatorClient.UserBeganPlaying), ((ISpectatorClient)this).UserBeganPlaying);
connection.On<int, FrameDataBundle>(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames);
connection.On<int, SpectatorState>(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying);
};
isConnected.BindTo(connector.IsConnected);
isConnected.BindValueChanged(connected =>
{ {
if (connected.NewValue) if (connected.NewValue)
{ {
@ -120,8 +99,8 @@ namespace osu.Game.Online.Spectator
WatchUser(userId); WatchUser(userId);
// re-send state in case it wasn't received // re-send state in case it wasn't received
if (isPlaying) if (IsPlaying)
beginPlaying(); BeginPlayingInternal(currentState);
} }
else else
{ {
@ -133,7 +112,6 @@ namespace osu.Game.Online.Spectator
} }
}, true); }, true);
} }
}
Task ISpectatorClient.UserBeganPlaying(int userId, SpectatorState state) Task ISpectatorClient.UserBeganPlaying(int userId, SpectatorState state)
{ {
@ -176,10 +154,10 @@ namespace osu.Game.Online.Spectator
public void BeginPlaying(GameplayBeatmap beatmap, Score score) public void BeginPlaying(GameplayBeatmap beatmap, Score score)
{ {
if (isPlaying) if (IsPlaying)
throw new InvalidOperationException($"Cannot invoke {nameof(BeginPlaying)} when already playing"); throw new InvalidOperationException($"Cannot invoke {nameof(BeginPlaying)} when already playing");
isPlaying = true; IsPlaying = true;
// transfer state at point of beginning play // transfer state at point of beginning play
currentState.BeatmapID = beatmap.BeatmapInfo.OnlineBeatmapID; currentState.BeatmapID = beatmap.BeatmapInfo.OnlineBeatmapID;
@ -189,36 +167,20 @@ namespace osu.Game.Online.Spectator
currentBeatmap = beatmap.PlayableBeatmap; currentBeatmap = beatmap.PlayableBeatmap;
currentScore = score; currentScore = score;
beginPlaying(); BeginPlayingInternal(currentState);
} }
private void beginPlaying() public void SendFrames(FrameDataBundle data) => lastSend = SendFramesInternal(data);
{
Debug.Assert(isPlaying);
if (!isConnected.Value) return;
connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), currentState);
}
public void SendFrames(FrameDataBundle data)
{
if (!isConnected.Value) return;
lastSend = connection.SendAsync(nameof(ISpectatorServer.SendFrameData), data);
}
public void EndPlaying() public void EndPlaying()
{ {
isPlaying = false; IsPlaying = false;
currentBeatmap = null; currentBeatmap = null;
if (!isConnected.Value) return; EndPlayingInternal(currentState);
connection.SendAsync(nameof(ISpectatorServer.EndPlaySession), currentState);
} }
public virtual void WatchUser(int userId) public void WatchUser(int userId)
{ {
lock (userLock) lock (userLock)
{ {
@ -226,32 +188,36 @@ namespace osu.Game.Online.Spectator
return; return;
watchingUsers.Add(userId); watchingUsers.Add(userId);
if (!isConnected.Value)
return;
} }
connection.SendAsync(nameof(ISpectatorServer.StartWatchingUser), userId); WatchUserInternal(userId);
} }
public virtual void StopWatchingUser(int userId) public void StopWatchingUser(int userId)
{ {
lock (userLock) lock (userLock)
{ {
watchingUsers.Remove(userId); watchingUsers.Remove(userId);
if (!isConnected.Value)
return;
} }
connection.SendAsync(nameof(ISpectatorServer.EndWatchingUser), userId); StopWatchingUserInternal(userId);
} }
protected abstract Task BeginPlayingInternal(SpectatorState state);
protected abstract Task SendFramesInternal(FrameDataBundle data);
protected abstract Task EndPlayingInternal(SpectatorState state);
protected abstract Task WatchUserInternal(int userId);
protected abstract Task StopWatchingUserInternal(int userId);
private readonly Queue<LegacyReplayFrame> pendingFrames = new Queue<LegacyReplayFrame>(); private readonly Queue<LegacyReplayFrame> pendingFrames = new Queue<LegacyReplayFrame>();
private double lastSendTime; private double lastSendTime;
private Task lastSend; private Task? lastSend;
private const int max_pending_frames = 30; private const int max_pending_frames = 30;

View File

@ -85,7 +85,7 @@ namespace osu.Game
protected IAPIProvider API; protected IAPIProvider API;
private SpectatorStreamingClient spectatorStreaming; private SpectatorClient spectatorClient;
private MultiplayerClient multiplayerClient; private MultiplayerClient multiplayerClient;
protected MenuCursorContainer MenuCursorContainer; protected MenuCursorContainer MenuCursorContainer;
@ -240,7 +240,7 @@ namespace osu.Game
dependencies.CacheAs(API ??= new APIAccess(LocalConfig, endpoints, VersionHash)); dependencies.CacheAs(API ??= new APIAccess(LocalConfig, endpoints, VersionHash));
dependencies.CacheAs(spectatorStreaming = new SpectatorStreamingClient(endpoints)); dependencies.CacheAs(spectatorClient = new OnlineSpectatorClient(endpoints));
dependencies.CacheAs(multiplayerClient = new OnlineMultiplayerClient(endpoints)); dependencies.CacheAs(multiplayerClient = new OnlineMultiplayerClient(endpoints));
var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures); var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures);
@ -313,7 +313,7 @@ namespace osu.Game
// add api components to hierarchy. // add api components to hierarchy.
if (API is APIAccess apiAccess) if (API is APIAccess apiAccess)
AddInternal(apiAccess); AddInternal(apiAccess);
AddInternal(spectatorStreaming); AddInternal(spectatorClient);
AddInternal(multiplayerClient); AddInternal(multiplayerClient);
AddInternal(RulesetConfigCache); AddInternal(RulesetConfigCache);

View File

@ -25,7 +25,7 @@ namespace osu.Game.Overlays.Dashboard
private FillFlowContainer<PlayingUserPanel> userFlow; private FillFlowContainer<PlayingUserPanel> userFlow;
[Resolved] [Resolved]
private SpectatorStreamingClient spectatorStreaming { get; set; } private SpectatorClient spectatorClient { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
@ -52,7 +52,7 @@ namespace osu.Game.Overlays.Dashboard
{ {
base.LoadComplete(); base.LoadComplete();
playingUsers.BindTo(spectatorStreaming.PlayingUsers); playingUsers.BindTo(spectatorClient.PlayingUsers);
playingUsers.BindCollectionChanged(onUsersChanged, true); playingUsers.BindCollectionChanged(onUsersChanged, true);
} }

View File

@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.UI
public int RecordFrameRate = 60; public int RecordFrameRate = 60;
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private SpectatorStreamingClient spectatorStreaming { get; set; } private SpectatorClient spectatorClient { get; set; }
[Resolved] [Resolved]
private GameplayBeatmap gameplayBeatmap { get; set; } private GameplayBeatmap gameplayBeatmap { get; set; }
@ -49,13 +49,13 @@ namespace osu.Game.Rulesets.UI
inputManager = GetContainingInputManager(); inputManager = GetContainingInputManager();
spectatorStreaming?.BeginPlaying(gameplayBeatmap, target); spectatorClient?.BeginPlaying(gameplayBeatmap, target);
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
spectatorStreaming?.EndPlaying(); spectatorClient?.EndPlaying();
} }
protected override void Update() protected override void Update()
@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.UI
{ {
target.Replay.Frames.Add(frame); target.Replay.Frames.Add(frame);
spectatorStreaming?.HandleFrame(frame); spectatorClient?.HandleFrame(frame);
} }
} }

View File

@ -28,7 +28,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
public bool AllPlayersLoaded => instances.All(p => p?.PlayerLoaded == true); public bool AllPlayersLoaded => instances.All(p => p?.PlayerLoaded == true);
[Resolved] [Resolved]
private SpectatorStreamingClient spectatorClient { get; set; } private SpectatorClient spectatorClient { get; set; }
[Resolved] [Resolved]
private MultiplayerClient multiplayerClient { get; set; } private MultiplayerClient multiplayerClient { get; set; }

View File

@ -22,7 +22,7 @@ namespace osu.Game.Screens.Play.HUD
protected readonly Dictionary<int, TrackedUserData> UserScores = new Dictionary<int, TrackedUserData>(); protected readonly Dictionary<int, TrackedUserData> UserScores = new Dictionary<int, TrackedUserData>();
[Resolved] [Resolved]
private SpectatorStreamingClient streamingClient { get; set; } private SpectatorClient spectatorClient { get; set; }
[Resolved] [Resolved]
private MultiplayerClient multiplayerClient { get; set; } private MultiplayerClient multiplayerClient { get; set; }
@ -55,7 +55,7 @@ namespace osu.Game.Screens.Play.HUD
foreach (var userId in playingUsers) foreach (var userId in playingUsers)
{ {
streamingClient.WatchUser(userId); spectatorClient.WatchUser(userId);
// probably won't be required in the final implementation. // probably won't be required in the final implementation.
var resolvedUser = userLookupCache.GetUserAsync(userId).Result; var resolvedUser = userLookupCache.GetUserAsync(userId).Result;
@ -88,7 +88,7 @@ namespace osu.Game.Screens.Play.HUD
playingUsers.BindCollectionChanged(usersChanged); playingUsers.BindCollectionChanged(usersChanged);
// this leaderboard should be guaranteed to be completely loaded before the gameplay starts (is a prerequisite in MultiplayerPlayer). // this leaderboard should be guaranteed to be completely loaded before the gameplay starts (is a prerequisite in MultiplayerPlayer).
streamingClient.OnNewFrames += handleIncomingFrames; spectatorClient.OnNewFrames += handleIncomingFrames;
} }
private void usersChanged(object sender, NotifyCollectionChangedEventArgs e) private void usersChanged(object sender, NotifyCollectionChangedEventArgs e)
@ -98,7 +98,7 @@ namespace osu.Game.Screens.Play.HUD
case NotifyCollectionChangedAction.Remove: case NotifyCollectionChangedAction.Remove:
foreach (var userId in e.OldItems.OfType<int>()) foreach (var userId in e.OldItems.OfType<int>())
{ {
streamingClient.StopWatchingUser(userId); spectatorClient.StopWatchingUser(userId);
if (UserScores.TryGetValue(userId, out var trackedData)) if (UserScores.TryGetValue(userId, out var trackedData))
trackedData.MarkUserQuit(); trackedData.MarkUserQuit();
@ -123,14 +123,14 @@ namespace osu.Game.Screens.Play.HUD
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
if (streamingClient != null) if (spectatorClient != null)
{ {
foreach (var user in playingUsers) foreach (var user in playingUsers)
{ {
streamingClient.StopWatchingUser(user); spectatorClient.StopWatchingUser(user);
} }
streamingClient.OnNewFrames -= handleIncomingFrames; spectatorClient.OnNewFrames -= handleIncomingFrames;
} }
} }

View File

@ -31,12 +31,12 @@ namespace osu.Game.Screens.Play
} }
[Resolved] [Resolved]
private SpectatorStreamingClient spectatorStreaming { get; set; } private SpectatorClient spectatorClient { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
spectatorStreaming.OnUserBeganPlaying += userBeganPlaying; spectatorClient.OnUserBeganPlaying += userBeganPlaying;
AddInternal(new OsuSpriteText AddInternal(new OsuSpriteText
{ {
@ -66,7 +66,7 @@ namespace osu.Game.Screens.Play
public override bool OnExiting(IScreen next) public override bool OnExiting(IScreen next)
{ {
spectatorStreaming.OnUserBeganPlaying -= userBeganPlaying; spectatorClient.OnUserBeganPlaying -= userBeganPlaying;
return base.OnExiting(next); return base.OnExiting(next);
} }
@ -84,8 +84,8 @@ namespace osu.Game.Screens.Play
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
if (spectatorStreaming != null) if (spectatorClient != null)
spectatorStreaming.OnUserBeganPlaying -= userBeganPlaying; spectatorClient.OnUserBeganPlaying -= userBeganPlaying;
} }
} }
} }

View File

@ -17,12 +17,12 @@ namespace osu.Game.Screens.Play
} }
[Resolved] [Resolved]
private SpectatorStreamingClient spectatorStreaming { get; set; } private SpectatorClient spectatorClient { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
spectatorStreaming.OnUserBeganPlaying += userBeganPlaying; spectatorClient.OnUserBeganPlaying += userBeganPlaying;
} }
private void userBeganPlaying(int userId, SpectatorState state) private void userBeganPlaying(int userId, SpectatorState state)
@ -40,8 +40,8 @@ namespace osu.Game.Screens.Play
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
if (spectatorStreaming != null) if (spectatorClient != null)
spectatorStreaming.OnUserBeganPlaying -= userBeganPlaying; spectatorClient.OnUserBeganPlaying -= userBeganPlaying;
} }
} }
} }

View File

@ -37,7 +37,7 @@ namespace osu.Game.Screens.Spectate
private RulesetStore rulesets { get; set; } private RulesetStore rulesets { get; set; }
[Resolved] [Resolved]
private SpectatorStreamingClient spectatorClient { get; set; } private SpectatorClient spectatorClient { get; set; }
[Resolved] [Resolved]
private UserLookupCache userLookupCache { get; set; } private UserLookupCache userLookupCache { get; set; }

View File

@ -0,0 +1,110 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Utils;
using osu.Game.Online.API;
using osu.Game.Online.Spectator;
using osu.Game.Replays.Legacy;
using osu.Game.Scoring;
namespace osu.Game.Tests.Visual.Spectator
{
public class TestSpectatorClient : SpectatorClient
{
public override IBindable<bool> IsConnected { get; } = new Bindable<bool>(true);
private readonly Dictionary<int, int> userBeatmapDictionary = new Dictionary<int, int>();
[Resolved]
private IAPIProvider api { get; set; } = null!;
/// <summary>
/// Starts play for an arbitrary user.
/// </summary>
/// <param name="userId">The user to start play for.</param>
/// <param name="beatmapId">The playing beatmap id.</param>
public void StartPlay(int userId, int beatmapId)
{
userBeatmapDictionary[userId] = beatmapId;
sendPlayingState(userId);
}
/// <summary>
/// Ends play for an arbitrary user.
/// </summary>
/// <param name="userId">The user to end play for.</param>
public void EndPlay(int userId)
{
if (!PlayingUsers.Contains(userId))
return;
((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState
{
BeatmapID = userBeatmapDictionary[userId],
RulesetID = 0,
});
}
/// <summary>
/// Sends frames for an arbitrary user.
/// </summary>
/// <param name="userId">The user to send frames for.</param>
/// <param name="index">The frame index.</param>
/// <param name="count">The number of frames to send.</param>
public void SendFrames(int userId, int index, int count)
{
var frames = new List<LegacyReplayFrame>();
for (int i = index; i < index + count; i++)
{
var buttonState = i == index + count - 1 ? ReplayButtonState.None : ReplayButtonState.Left1;
frames.Add(new LegacyReplayFrame(i * 100, RNG.Next(0, 512), RNG.Next(0, 512), buttonState));
}
var bundle = new FrameDataBundle(new ScoreInfo { Combo = index + count }, frames);
((ISpectatorClient)this).UserSentFrames(userId, bundle);
}
protected override Task BeginPlayingInternal(SpectatorState state)
{
// Track the local user's playing beatmap ID.
Debug.Assert(state.BeatmapID != null);
userBeatmapDictionary[api.LocalUser.Value.Id] = state.BeatmapID.Value;
return ((ISpectatorClient)this).UserBeganPlaying(api.LocalUser.Value.Id, state);
}
protected override Task SendFramesInternal(FrameDataBundle data) => ((ISpectatorClient)this).UserSentFrames(api.LocalUser.Value.Id, data);
protected override Task EndPlayingInternal(SpectatorState state) => ((ISpectatorClient)this).UserFinishedPlaying(api.LocalUser.Value.Id, state);
protected override Task WatchUserInternal(int userId)
{
// When newly watching a user, the server sends the playing state immediately.
if (PlayingUsers.Contains(userId))
sendPlayingState(userId);
return Task.CompletedTask;
}
protected override Task StopWatchingUserInternal(int userId) => Task.CompletedTask;
private void sendPlayingState(int userId)
{
((ISpectatorClient)this).UserBeganPlaying(userId, new SpectatorState
{
BeatmapID = userBeatmapDictionary[userId],
RulesetID = 0,
});
}
}
}

View File

@ -1,90 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Concurrent;
using System.Collections.Generic;
using osu.Framework.Bindables;
using osu.Framework.Utils;
using osu.Game.Online;
using osu.Game.Online.Spectator;
using osu.Game.Replays.Legacy;
using osu.Game.Scoring;
namespace osu.Game.Tests.Visual.Spectator
{
public class TestSpectatorStreamingClient : SpectatorStreamingClient
{
public new BindableList<int> PlayingUsers => (BindableList<int>)base.PlayingUsers;
private readonly ConcurrentDictionary<int, byte> watchingUsers = new ConcurrentDictionary<int, byte>();
private readonly Dictionary<int, int> userBeatmapDictionary = new Dictionary<int, int>();
private readonly Dictionary<int, bool> userSentStateDictionary = new Dictionary<int, bool>();
public TestSpectatorStreamingClient()
: base(new DevelopmentEndpointConfiguration())
{
}
public void StartPlay(int userId, int beatmapId)
{
userBeatmapDictionary[userId] = beatmapId;
sendState(userId, beatmapId);
}
public void EndPlay(int userId, int beatmapId)
{
((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState
{
BeatmapID = beatmapId,
RulesetID = 0,
});
userBeatmapDictionary.Remove(userId);
userSentStateDictionary.Remove(userId);
}
public void SendFrames(int userId, int index, int count)
{
var frames = new List<LegacyReplayFrame>();
for (int i = index; i < index + count; i++)
{
var buttonState = i == index + count - 1 ? ReplayButtonState.None : ReplayButtonState.Left1;
frames.Add(new LegacyReplayFrame(i * 100, RNG.Next(0, 512), RNG.Next(0, 512), buttonState));
}
var bundle = new FrameDataBundle(new ScoreInfo { Combo = index + count }, frames);
((ISpectatorClient)this).UserSentFrames(userId, bundle);
if (!userSentStateDictionary[userId])
sendState(userId, userBeatmapDictionary[userId]);
}
public override void WatchUser(int userId)
{
base.WatchUser(userId);
// When newly watching a user, the server sends the playing state immediately.
if (watchingUsers.TryAdd(userId, 0) && PlayingUsers.Contains(userId))
sendState(userId, userBeatmapDictionary[userId]);
}
public override void StopWatchingUser(int userId)
{
base.StopWatchingUser(userId);
watchingUsers.TryRemove(userId, out _);
}
private void sendState(int userId, int beatmapId)
{
((ISpectatorClient)this).UserBeganPlaying(userId, new SpectatorState
{
BeatmapID = beatmapId,
RulesetID = 0,
});
userSentStateDictionary[userId] = true;
}
}
}