From 1d7d8bd6fccba84c32f004caddaf3fb0a3bdfe33 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 23:26:31 +0900 Subject: [PATCH] Hook up a realtime multiplayer client --- .../RealtimeMultiplayerClient.cs | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs diff --git a/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs new file mode 100644 index 0000000000..6f18e1c922 --- /dev/null +++ b/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs @@ -0,0 +1,171 @@ +// Copyright (c) ppy Pty Ltd . 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; + +namespace osu.Game.Online.RealtimeMultiplayer +{ + public class RealtimeMultiplayerClient : StatefulMultiplayerClient + { + private const string endpoint = "https://spectator.ppy.sh/multiplayer"; + + public override IBindable IsConnected => isConnected; + + private readonly Bindable isConnected = new Bindable(); + private readonly IBindable apiState = new Bindable(); + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private HubConnection? connection; + + [BackgroundDependencyLoader] + private void load() + { + apiState.BindTo(api.State); + apiState.BindValueChanged(apiStateChanged, true); + } + + private void apiStateChanged(ValueChangedEvent state) + { + switch (state.NewValue) + { + 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; + + connection = 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 + connection.On(nameof(IMultiplayerClient.RoomStateChanged), ((IMultiplayerClient)this).RoomStateChanged); + connection.On(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined); + connection.On(nameof(IMultiplayerClient.UserLeft), ((IMultiplayerClient)this).UserLeft); + connection.On(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged); + connection.On(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged); + connection.On(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.Closed += async ex => + { + isConnected.Value = false; + + if (ex != null) + { + Logger.Log($"Multiplayer client lost connection: {ex}", LoggingTarget.Network); + await tryUntilConnected(); + } + }; + + await tryUntilConnected(); + + async Task tryUntilConnected() + { + Logger.Log("Multiplayer client connecting...", LoggingTarget.Network); + + while (api.State.Value == APIState.Online) + { + try + { + // reconnect on any failure + await connection.StartAsync(); + Logger.Log("Multiplayer client connected!", LoggingTarget.Network); + + // Success. + isConnected.Value = true; + break; + } + catch (Exception e) + { + Logger.Log($"Multiplayer client connection error: {e}", LoggingTarget.Network); + await Task.Delay(5000); + } + } + } + } + + protected override Task JoinRoom(long roomId) + { + if (!isConnected.Value) + return Task.FromCanceled(CancellationToken.None); + + return connection.InvokeAsync(nameof(IMultiplayerServer.JoinRoom), roomId); + } + + public override async Task LeaveRoom() + { + if (!isConnected.Value) + return; + + 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 StartMatch() + { + if (!isConnected.Value) + return Task.CompletedTask; + + return connection.InvokeAsync(nameof(IMultiplayerServer.StartMatch)); + } + } +}