1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 14:25:05 +08:00

Merge pull request #11709 from frenzibyte/abstract-hub-connection

Share connection logic between SignalR clients
This commit is contained in:
Dan Balasescu 2021-02-15 14:34:42 +09:00 committed by GitHub
commit 4e908adfb1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 276 additions and 274 deletions

View File

@ -3,7 +3,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -13,6 +12,7 @@ using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.Spectator;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Osu;
@ -244,10 +244,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
}
protected override Task Connect()
{
return Task.CompletedTask;
}
protected override HubClientConnector CreateConnector(string name, string endpoint, IAPIProvider api) => null;
public void StartPlay(int beatmapId)
{

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -14,6 +13,7 @@ using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Database;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.Spectator;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Osu.Scoring;
@ -106,6 +106,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
this.totalUsers = totalUsers;
}
protected override HubClientConnector CreateConnector(string name, string endpoint, IAPIProvider api) => null;
public void Start(int beatmapId)
{
for (int i = 0; i < totalUsers; i++)
@ -163,8 +165,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
((ISpectatorClient)this).UserSentFrames(userId, new FrameDataBundle(header, Array.Empty<LegacyReplayFrame>()));
}
}
protected override Task Connect() => Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,204 @@
// 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;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using osu.Framework;
using osu.Framework.Bindables;
using osu.Framework.Logging;
using osu.Game.Online.API;
namespace osu.Game.Online
{
/// <summary>
/// A component that manages the life cycle of a connection to a SignalR Hub.
/// </summary>
public class HubClientConnector : IDisposable
{
/// <summary>
/// Invoked whenever a new hub connection is built, to configure it before it's started.
/// </summary>
public Action<HubConnection>? ConfigureConnection;
private readonly string clientName;
private readonly string endpoint;
private readonly IAPIProvider api;
/// <summary>
/// The current connection opened by this connector.
/// </summary>
public HubConnection? CurrentConnection { get; private set; }
/// <summary>
/// Whether this is connected to the hub, use <see cref="CurrentConnection"/> to access the connection, if this is <c>true</c>.
/// </summary>
public IBindable<bool> IsConnected => isConnected;
private readonly Bindable<bool> isConnected = new Bindable<bool>();
private readonly SemaphoreSlim connectionLock = new SemaphoreSlim(1);
private CancellationTokenSource connectCancelSource = new CancellationTokenSource();
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
/// <summary>
/// Constructs a new <see cref="HubClientConnector"/>.
/// </summary>
/// <param name="clientName">The name of the client this connector connects for, used for logging.</param>
/// <param name="endpoint">The endpoint to the hub.</param>
/// <param name="api"> An API provider used to react to connection state changes.</param>
public HubClientConnector(string clientName, string endpoint, IAPIProvider api)
{
this.clientName = clientName;
this.endpoint = endpoint;
this.api = api;
apiState.BindTo(api.State);
apiState.BindValueChanged(state =>
{
switch (state.NewValue)
{
case APIState.Failing:
case APIState.Offline:
Task.Run(() => disconnect(true));
break;
case APIState.Online:
Task.Run(connect);
break;
}
}, true);
}
private async Task connect()
{
cancelExistingConnect();
if (!await connectionLock.WaitAsync(10000))
throw new TimeoutException("Could not obtain a lock to connect. A previous attempt is likely stuck.");
try
{
while (apiState.Value == APIState.Online)
{
// ensure any previous connection was disposed.
// this will also create a new cancellation token source.
await disconnect(false);
// this token will be valid for the scope of this connection.
// if cancelled, we can be sure that a disconnect or reconnect is handled elsewhere.
var cancellationToken = connectCancelSource.Token;
cancellationToken.ThrowIfCancellationRequested();
Logger.Log($"{clientName} connecting...", LoggingTarget.Network);
try
{
// importantly, rebuild the connection each attempt to get an updated access token.
CurrentConnection = buildConnection(cancellationToken);
await CurrentConnection.StartAsync(cancellationToken);
Logger.Log($"{clientName} connected!", LoggingTarget.Network);
isConnected.Value = true;
return;
}
catch (OperationCanceledException)
{
//connection process was cancelled.
throw;
}
catch (Exception e)
{
Logger.Log($"{clientName} connection error: {e}", LoggingTarget.Network);
// retry on any failure.
await Task.Delay(5000, cancellationToken);
}
}
}
finally
{
connectionLock.Release();
}
}
private HubConnection buildConnection(CancellationToken cancellationToken)
{
var builder = new HubConnectionBuilder()
.WithUrl(endpoint, options => { options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); });
if (RuntimeInfo.SupportsJIT)
builder.AddMessagePackProtocol();
else
{
// eventually we will precompile resolvers for messagepack, but this isn't working currently
// see https://github.com/neuecc/MessagePack-CSharp/issues/780#issuecomment-768794308.
builder.AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; });
}
var newConnection = builder.Build();
ConfigureConnection?.Invoke(newConnection);
newConnection.Closed += ex => onConnectionClosed(ex, cancellationToken);
return newConnection;
}
private Task onConnectionClosed(Exception? ex, CancellationToken cancellationToken)
{
isConnected.Value = false;
Logger.Log(ex != null ? $"{clientName} lost connection: {ex}" : $"{clientName} disconnected", LoggingTarget.Network);
// make sure a disconnect wasn't triggered (and this is still the active connection).
if (!cancellationToken.IsCancellationRequested)
Task.Run(connect, default);
return Task.CompletedTask;
}
private async Task disconnect(bool takeLock)
{
cancelExistingConnect();
if (takeLock)
{
if (!await connectionLock.WaitAsync(10000))
throw new TimeoutException("Could not obtain a lock to disconnect. A previous attempt is likely stuck.");
}
try
{
if (CurrentConnection != null)
await CurrentConnection.DisposeAsync();
}
finally
{
CurrentConnection = null;
if (takeLock)
connectionLock.Release();
}
}
private void cancelExistingConnect()
{
connectCancelSource.Cancel();
connectCancelSource = new CancellationTokenSource();
}
public override string ToString() => $"Connector for {clientName} ({(IsConnected.Value ? "connected" : "not connected")}";
public void Dispose()
{
apiState.UnbindAll();
cancelExistingConnect();
}
}
}

View File

@ -3,17 +3,12 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Logging;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
@ -21,21 +16,12 @@ namespace osu.Game.Online.Multiplayer
{
public class MultiplayerClient : StatefulMultiplayerClient
{
public override IBindable<bool> IsConnected => isConnected;
private readonly Bindable<bool> isConnected = new Bindable<bool>();
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
private readonly SemaphoreSlim connectionLock = new SemaphoreSlim(1);
[Resolved]
private IAPIProvider api { get; set; } = null!;
private HubConnection? connection;
private CancellationTokenSource connectCancelSource = new CancellationTokenSource();
private readonly string endpoint;
private HubClientConnector? connector;
public override IBindable<bool> IsConnected { get; } = new BindableBool();
private HubConnection? connection => connector?.CurrentConnection;
public MultiplayerClient(EndpointConfiguration endpoints)
{
@ -43,84 +29,34 @@ namespace osu.Game.Online.Multiplayer
}
[BackgroundDependencyLoader]
private void load()
private void load(IAPIProvider api)
{
apiState.BindTo(api.State);
apiState.BindValueChanged(apiStateChanged, true);
}
private void apiStateChanged(ValueChangedEvent<APIState> state)
{
switch (state.NewValue)
connector = new HubClientConnector(nameof(MultiplayerClient), endpoint, api)
{
case APIState.Failing:
case APIState.Offline:
Task.Run(() => disconnect(true));
break;
case APIState.Online:
Task.Run(connect);
break;
}
}
private async Task connect()
{
cancelExistingConnect();
if (!await connectionLock.WaitAsync(10000))
throw new TimeoutException("Could not obtain a lock to connect. A previous attempt is likely stuck.");
try
{
while (api.State.Value == APIState.Online)
ConfigureConnection = connection =>
{
// ensure any previous connection was disposed.
// this will also create a new cancellation token source.
await disconnect(false);
// this is kind of SILLY
// https://github.com/dotnet/aspnetcore/issues/15198
connection.On<MultiplayerRoomState>(nameof(IMultiplayerClient.RoomStateChanged), ((IMultiplayerClient)this).RoomStateChanged);
connection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined);
connection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserLeft), ((IMultiplayerClient)this).UserLeft);
connection.On<int>(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged);
connection.On<MultiplayerRoomSettings>(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged);
connection.On<int, MultiplayerUserState>(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged);
connection.On(nameof(IMultiplayerClient.LoadRequested), ((IMultiplayerClient)this).LoadRequested);
connection.On(nameof(IMultiplayerClient.MatchStarted), ((IMultiplayerClient)this).MatchStarted);
connection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady);
connection.On<int, IEnumerable<APIMod>>(nameof(IMultiplayerClient.UserModsChanged), ((IMultiplayerClient)this).UserModsChanged);
connection.On<int, BeatmapAvailability>(nameof(IMultiplayerClient.UserBeatmapAvailabilityChanged), ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged);
},
};
// this token will be valid for the scope of this connection.
// if cancelled, we can be sure that a disconnect or reconnect is handled elsewhere.
var cancellationToken = connectCancelSource.Token;
cancellationToken.ThrowIfCancellationRequested();
Logger.Log("Multiplayer client connecting...", LoggingTarget.Network);
try
{
// importantly, rebuild the connection each attempt to get an updated access token.
connection = createConnection(cancellationToken);
await connection.StartAsync(cancellationToken);
Logger.Log("Multiplayer client connected!", LoggingTarget.Network);
isConnected.Value = true;
return;
}
catch (OperationCanceledException)
{
//connection process was cancelled.
throw;
}
catch (Exception e)
{
Logger.Log($"Multiplayer client connection error: {e}", LoggingTarget.Network);
// retry on any failure.
await Task.Delay(5000, cancellationToken);
}
}
}
finally
{
connectionLock.Release();
}
IsConnected.BindTo(connector.IsConnected);
}
protected override Task<MultiplayerRoom> JoinRoom(long roomId)
{
if (!isConnected.Value)
if (!IsConnected.Value)
return Task.FromCanceled<MultiplayerRoom>(new CancellationToken(true));
return connection.InvokeAsync<MultiplayerRoom>(nameof(IMultiplayerServer.JoinRoom), roomId);
@ -128,7 +64,7 @@ namespace osu.Game.Online.Multiplayer
protected override Task LeaveRoomInternal()
{
if (!isConnected.Value)
if (!IsConnected.Value)
return Task.FromCanceled(new CancellationToken(true));
return connection.InvokeAsync(nameof(IMultiplayerServer.LeaveRoom));
@ -136,7 +72,7 @@ namespace osu.Game.Online.Multiplayer
public override Task TransferHost(int userId)
{
if (!isConnected.Value)
if (!IsConnected.Value)
return Task.CompletedTask;
return connection.InvokeAsync(nameof(IMultiplayerServer.TransferHost), userId);
@ -144,7 +80,7 @@ namespace osu.Game.Online.Multiplayer
public override Task ChangeSettings(MultiplayerRoomSettings settings)
{
if (!isConnected.Value)
if (!IsConnected.Value)
return Task.CompletedTask;
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeSettings), settings);
@ -152,7 +88,7 @@ namespace osu.Game.Online.Multiplayer
public override Task ChangeState(MultiplayerUserState newState)
{
if (!isConnected.Value)
if (!IsConnected.Value)
return Task.CompletedTask;
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeState), newState);
@ -160,7 +96,7 @@ namespace osu.Game.Online.Multiplayer
public override Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability)
{
if (!isConnected.Value)
if (!IsConnected.Value)
return Task.CompletedTask;
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeBeatmapAvailability), newBeatmapAvailability);
@ -168,7 +104,7 @@ namespace osu.Game.Online.Multiplayer
public override Task ChangeUserMods(IEnumerable<APIMod> newMods)
{
if (!isConnected.Value)
if (!IsConnected.Value)
return Task.CompletedTask;
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeUserMods), newMods);
@ -176,91 +112,16 @@ namespace osu.Game.Online.Multiplayer
public override Task StartMatch()
{
if (!isConnected.Value)
if (!IsConnected.Value)
return Task.CompletedTask;
return connection.InvokeAsync(nameof(IMultiplayerServer.StartMatch));
}
private async Task disconnect(bool takeLock)
{
cancelExistingConnect();
if (takeLock)
{
if (!await connectionLock.WaitAsync(10000))
throw new TimeoutException("Could not obtain a lock to disconnect. A previous attempt is likely stuck.");
}
try
{
if (connection != null)
await connection.DisposeAsync();
}
finally
{
connection = null;
if (takeLock)
connectionLock.Release();
}
}
private void cancelExistingConnect()
{
connectCancelSource.Cancel();
connectCancelSource = new CancellationTokenSource();
}
private HubConnection createConnection(CancellationToken cancellationToken)
{
var builder = new HubConnectionBuilder()
.WithUrl(endpoint, options => { options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); });
if (RuntimeInfo.SupportsJIT)
builder.AddMessagePackProtocol();
else
{
// eventually we will precompile resolvers for messagepack, but this isn't working currently
// see https://github.com/neuecc/MessagePack-CSharp/issues/780#issuecomment-768794308.
builder.AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; });
}
var newConnection = builder.Build();
// this is kind of SILLY
// https://github.com/dotnet/aspnetcore/issues/15198
newConnection.On<MultiplayerRoomState>(nameof(IMultiplayerClient.RoomStateChanged), ((IMultiplayerClient)this).RoomStateChanged);
newConnection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined);
newConnection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserLeft), ((IMultiplayerClient)this).UserLeft);
newConnection.On<int>(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged);
newConnection.On<MultiplayerRoomSettings>(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged);
newConnection.On<int, MultiplayerUserState>(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged);
newConnection.On(nameof(IMultiplayerClient.LoadRequested), ((IMultiplayerClient)this).LoadRequested);
newConnection.On(nameof(IMultiplayerClient.MatchStarted), ((IMultiplayerClient)this).MatchStarted);
newConnection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady);
newConnection.On<int, IEnumerable<APIMod>>(nameof(IMultiplayerClient.UserModsChanged), ((IMultiplayerClient)this).UserModsChanged);
newConnection.On<int, BeatmapAvailability>(nameof(IMultiplayerClient.UserBeatmapAvailabilityChanged), ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged);
newConnection.Closed += ex =>
{
isConnected.Value = false;
Logger.Log(ex != null ? $"Multiplayer client lost connection: {ex}" : "Multiplayer client disconnected", LoggingTarget.Network);
// make sure a disconnect wasn't triggered (and this is still the active connection).
if (!cancellationToken.IsCancellationRequested)
Task.Run(connect, default);
return Task.CompletedTask;
};
return newConnection;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
cancelExistingConnect();
connector?.Dispose();
}
}
}

View File

@ -97,7 +97,8 @@ namespace osu.Game.Online.Multiplayer
// Todo: This is temporary, until the multiplayer server returns the item id on match start or otherwise.
private int playlistItemId;
protected StatefulMultiplayerClient()
[BackgroundDependencyLoader]
private void load()
{
IsConnected.BindValueChanged(connected =>
{

View File

@ -8,13 +8,9 @@ using System.Linq;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Replays.Legacy;
@ -34,7 +30,14 @@ namespace osu.Game.Online.Spectator
/// </summary>
public const double TIME_BETWEEN_SENDS = 200;
private HubConnection connection;
private readonly string endpoint;
[CanBeNull]
private HubClientConnector connector;
private readonly IBindable<bool> isConnected = new BindableBool();
private HubConnection connection => connector?.CurrentConnection;
private readonly List<int> watchingUsers = new List<int>();
@ -44,13 +47,6 @@ namespace osu.Game.Online.Spectator
private readonly BindableList<int> playingUsers = new BindableList<int>();
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
private bool isConnected;
[Resolved]
private IAPIProvider api { get; set; }
[CanBeNull]
private IBeatmap currentBeatmap;
@ -82,85 +78,32 @@ namespace osu.Game.Online.Spectator
/// </summary>
public event Action<int, SpectatorState> OnUserFinishedPlaying;
private readonly string endpoint;
public SpectatorStreamingClient(EndpointConfiguration endpoints)
{
endpoint = endpoints.SpectatorEndpointUrl;
}
[BackgroundDependencyLoader]
private void load()
private void load(IAPIProvider api)
{
apiState.BindTo(api.State);
apiState.BindValueChanged(apiStateChanged, true);
}
connector = CreateConnector(nameof(SpectatorStreamingClient), endpoint, api);
private void apiStateChanged(ValueChangedEvent<APIState> state)
{
switch (state.NewValue)
if (connector != null)
{
case APIState.Failing:
case APIState.Offline:
connection?.StopAsync();
connection = null;
break;
case APIState.Online:
Task.Run(Connect);
break;
}
}
protected virtual async Task Connect()
{
if (connection != null)
return;
var builder = new HubConnectionBuilder()
.WithUrl(endpoint, options => { options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); });
if (RuntimeInfo.SupportsJIT)
builder.AddMessagePackProtocol();
else
{
// eventually we will precompile resolvers for messagepack, but this isn't working currently
// see https://github.com/neuecc/MessagePack-CSharp/issues/780#issuecomment-768794308.
builder.AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; });
}
connection = builder.Build();
// 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);
connection.Closed += async ex =>
{
isConnected = false;
playingUsers.Clear();
if (ex != null)
connector.ConfigureConnection = connection =>
{
Logger.Log($"Spectator client lost connection: {ex}", LoggingTarget.Network);
await tryUntilConnected();
}
};
// 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);
};
await tryUntilConnected();
async Task tryUntilConnected()
{
Logger.Log("Spectator client connecting...", LoggingTarget.Network);
while (api.State.Value == APIState.Online)
isConnected.BindTo(connector.IsConnected);
isConnected.BindValueChanged(connected =>
{
try
if (connected.NewValue)
{
// reconnect on any failure
await connection.StartAsync();
Logger.Log("Spectator client connected!", LoggingTarget.Network);
// get all the users that were previously being watched
int[] users;
@ -170,28 +113,24 @@ namespace osu.Game.Online.Spectator
watchingUsers.Clear();
}
// success
isConnected = true;
// resubscribe to watched users
// resubscribe to watched users.
foreach (var userId in users)
WatchUser(userId);
// re-send state in case it wasn't received
if (isPlaying)
beginPlaying();
break;
}
catch (Exception e)
else
{
Logger.Log($"Spectator client connection error: {e}", LoggingTarget.Network);
await Task.Delay(5000);
playingUsers.Clear();
}
}
}, true);
}
}
protected virtual HubClientConnector CreateConnector(string name, string endpoint, IAPIProvider api) => new HubClientConnector(name, endpoint, api);
Task ISpectatorClient.UserBeganPlaying(int userId, SpectatorState state)
{
if (!playingUsers.Contains(userId))
@ -240,14 +179,14 @@ namespace osu.Game.Online.Spectator
{
Debug.Assert(isPlaying);
if (!isConnected) return;
if (!isConnected.Value) return;
connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), currentState);
}
public void SendFrames(FrameDataBundle data)
{
if (!isConnected) return;
if (!isConnected.Value) return;
lastSend = connection.SendAsync(nameof(ISpectatorServer.SendFrameData), data);
}
@ -257,7 +196,7 @@ namespace osu.Game.Online.Spectator
isPlaying = false;
currentBeatmap = null;
if (!isConnected) return;
if (!isConnected.Value) return;
connection.SendAsync(nameof(ISpectatorServer.EndPlaySession), currentState);
}
@ -271,7 +210,7 @@ namespace osu.Game.Online.Spectator
watchingUsers.Add(userId);
if (!isConnected)
if (!isConnected.Value)
return;
}
@ -284,7 +223,7 @@ namespace osu.Game.Online.Spectator
{
watchingUsers.Remove(userId);
if (!isConnected)
if (!isConnected.Value)
return;
}