1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-07 12:27:25 +08:00
osu-lazer/osu.Game/Online/Multiplayer/MultiplayerClient.cs

239 lines
8.6 KiB
C#
Raw Normal View History

2020-12-20 22:26:31 +08:00
// 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.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Logging;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
2020-12-20 22:26:31 +08:00
2020-12-25 12:38:11 +08:00
namespace osu.Game.Online.Multiplayer
2020-12-20 22:26:31 +08:00
{
2020-12-25 12:38:11 +08:00
public class MultiplayerClient : StatefulMultiplayerClient
2020-12-20 22:26:31 +08:00
{
public override IBindable<bool> IsConnected => isConnected;
private readonly Bindable<bool> isConnected = new Bindable<bool>();
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
[Resolved]
private IAPIProvider api { get; set; } = null!;
private HubConnection? connection;
private CancellationTokenSource connectCancelSource = new CancellationTokenSource();
private readonly string endpoint;
2020-12-25 12:38:11 +08:00
public MultiplayerClient(EndpointConfiguration endpoints)
{
endpoint = endpoints.MultiplayerEndpointUrl;
}
2020-12-20 22:26:31 +08:00
[BackgroundDependencyLoader]
private void load()
{
apiState.BindTo(api.State);
apiState.BindValueChanged(apiStateChanged, true);
}
private void apiStateChanged(ValueChangedEvent<APIState> state)
{
switch (state.NewValue)
{
case APIState.Failing:
case APIState.Offline:
Task.Run(Disconnect);
2020-12-20 22:26:31 +08:00
break;
case APIState.Online:
Task.Run(Connect);
break;
}
}
private readonly SemaphoreSlim connectionLock = new SemaphoreSlim(1);
2020-12-20 22:26:31 +08:00
public Task Disconnect() => disconnect(true);
protected async Task Connect()
{
cancelExistingConnect();
2020-12-20 22:26:31 +08:00
await connectionLock.WaitAsync();
2020-12-20 22:26:31 +08:00
try
2020-12-20 22:26:31 +08:00
{
await disconnect(false);
2020-12-20 22:26:31 +08:00
// 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;
while (api.State.Value == APIState.Online && !cancellationToken.IsCancellationRequested)
2020-12-20 22:26:31 +08:00
{
Logger.Log("Multiplayer client connecting...", LoggingTarget.Network);
2020-12-20 22:26:31 +08:00
try
{
// importantly, rebuild the connection each attempt to get an updated access token.
connection = createConnection(cancellationToken);
await connection.StartAsync(cancellationToken);
2020-12-20 22:26:31 +08:00
Logger.Log("Multiplayer client connected!", LoggingTarget.Network);
2020-12-20 22:26:31 +08:00
isConnected.Value = true;
return;
}
catch (OperationCanceledException)
{
//connection process was cancelled.
return;
2020-12-20 22:26:31 +08:00
}
catch (Exception e)
{
Logger.Log($"Multiplayer client connection error: {e}", LoggingTarget.Network);
// retry on any failure.
await Task.Delay(5000, cancellationToken);
2020-12-20 22:26:31 +08:00
}
}
}
finally
{
connectionLock.Release();
}
2020-12-20 22:26:31 +08:00
}
protected override Task<MultiplayerRoom> JoinRoom(long roomId)
{
if (!isConnected.Value)
return Task.FromCanceled<MultiplayerRoom>(new CancellationToken(true));
2020-12-20 22:26:31 +08:00
return connection.InvokeAsync<MultiplayerRoom>(nameof(IMultiplayerServer.JoinRoom), roomId);
}
public override async Task LeaveRoom()
{
if (!isConnected.Value)
{
// even if not connected, make sure the local room state can be cleaned up.
await base.LeaveRoom();
2020-12-20 22:26:31 +08:00
return;
}
2020-12-20 22:26:31 +08:00
if (Room == null)
return;
await base.LeaveRoom();
await connection.InvokeAsync(nameof(IMultiplayerServer.LeaveRoom));
}
public override Task TransferHost(int userId)
{
if (!isConnected.Value)
return Task.CompletedTask;
return connection.InvokeAsync(nameof(IMultiplayerServer.TransferHost), userId);
}
public override Task ChangeSettings(MultiplayerRoomSettings settings)
{
if (!isConnected.Value)
return Task.CompletedTask;
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeSettings), settings);
}
public override Task ChangeState(MultiplayerUserState newState)
{
if (!isConnected.Value)
return Task.CompletedTask;
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeState), newState);
}
public override Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability)
{
if (!isConnected.Value)
return Task.CompletedTask;
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeBeatmapAvailability), newBeatmapAvailability);
}
2020-12-20 22:26:31 +08:00
public override Task StartMatch()
{
if (!isConnected.Value)
return Task.CompletedTask;
return connection.InvokeAsync(nameof(IMultiplayerServer.StartMatch));
}
private async Task disconnect(bool takeLock)
{
cancelExistingConnect();
if (takeLock)
await connectionLock.WaitAsync();
try
{
if (connection != null)
await connection.StopAsync();
}
finally
{
connection = null;
if (takeLock)
connectionLock.Release();
}
}
private void cancelExistingConnect()
{
connectCancelSource.Cancel();
connectCancelSource = new CancellationTokenSource();
}
private HubConnection createConnection(CancellationToken cancellationToken)
{
var newConnection = new HubConnectionBuilder()
.WithUrl(endpoint, options => { options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); })
.AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; })
.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.Closed += async 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)
await Connect();
};
return newConnection;
}
2020-12-20 22:26:31 +08:00
}
}