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:
commit
4e908adfb1
@ -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)
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
204
osu.Game/Online/HubClientConnector.cs
Normal file
204
osu.Game/Online/HubClientConnector.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 =>
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user