1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-17 12:22:55 +08:00

Merge branch 'master' into beatmap-info

This commit is contained in:
C0D3 M4513R 2022-11-02 15:38:15 +00:00 committed by GitHub
commit 49427fe8b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 286 additions and 180 deletions

View File

@ -0,0 +1,28 @@
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Client;
namespace osu.Game.Online
{
public class HubClient : PersistentEndpointClient
{
public readonly HubConnection Connection;
public HubClient(HubConnection connection)
{
Connection = connection;
Connection.Closed += InvokeClosed;
}
public override Task ConnectAsync(CancellationToken cancellationToken) => Connection.StartAsync(cancellationToken);
public override async ValueTask DisposeAsync()
{
await base.DisposeAsync().ConfigureAwait(false);
await Connection.DisposeAsync().ConfigureAwait(false);
}
}
}

View File

@ -10,13 +10,11 @@ using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework; using osu.Framework;
using osu.Framework.Bindables;
using osu.Framework.Logging;
using osu.Game.Online.API; using osu.Game.Online.API;
namespace osu.Game.Online namespace osu.Game.Online
{ {
public class HubClientConnector : IHubClientConnector public class HubClientConnector : PersistentEndpointClientConnector, IHubClientConnector
{ {
public const string SERVER_SHUTDOWN_MESSAGE = "Server is shutting down."; public const string SERVER_SHUTDOWN_MESSAGE = "Server is shutting down.";
@ -25,7 +23,6 @@ namespace osu.Game.Online
/// </summary> /// </summary>
public Action<HubConnection>? ConfigureConnection { get; set; } public Action<HubConnection>? ConfigureConnection { get; set; }
private readonly string clientName;
private readonly string endpoint; private readonly string endpoint;
private readonly string versionHash; private readonly string versionHash;
private readonly bool preferMessagePack; private readonly bool preferMessagePack;
@ -34,18 +31,7 @@ namespace osu.Game.Online
/// <summary> /// <summary>
/// The current connection opened by this connector. /// The current connection opened by this connector.
/// </summary> /// </summary>
public HubConnection? CurrentConnection { get; private set; } public new HubConnection? CurrentConnection => ((HubClient?)base.CurrentConnection)?.Connection;
/// <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> /// <summary>
/// Constructs a new <see cref="HubClientConnector"/>. /// Constructs a new <see cref="HubClientConnector"/>.
@ -56,99 +42,16 @@ namespace osu.Game.Online
/// <param name="versionHash">The hash representing the current game version, used for verification purposes.</param> /// <param name="versionHash">The hash representing the current game version, used for verification purposes.</param>
/// <param name="preferMessagePack">Whether to use MessagePack for serialisation if available on this platform.</param> /// <param name="preferMessagePack">Whether to use MessagePack for serialisation if available on this platform.</param>
public HubClientConnector(string clientName, string endpoint, IAPIProvider api, string versionHash, bool preferMessagePack = true) public HubClientConnector(string clientName, string endpoint, IAPIProvider api, string versionHash, bool preferMessagePack = true)
: base(api)
{ {
this.clientName = clientName; ClientName = clientName;
this.endpoint = endpoint; this.endpoint = endpoint;
this.api = api; this.api = api;
this.versionHash = versionHash; this.versionHash = versionHash;
this.preferMessagePack = preferMessagePack; this.preferMessagePack = preferMessagePack;
apiState.BindTo(api.State);
apiState.BindValueChanged(_ => Task.Run(connectIfPossible), true);
} }
public Task Reconnect() protected override Task<PersistentEndpointClient> BuildConnectionAsync(CancellationToken cancellationToken)
{
Logger.Log($"{clientName} reconnecting...", LoggingTarget.Network);
return Task.Run(connectIfPossible);
}
private async Task connectIfPossible()
{
switch (apiState.Value)
{
case APIState.Failing:
case APIState.Offline:
await disconnect(true);
break;
case APIState.Online:
await connect();
break;
}
}
private async Task connect()
{
cancelExistingConnect();
if (!await connectionLock.WaitAsync(10000).ConfigureAwait(false))
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).ConfigureAwait(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).ConfigureAwait(false);
Logger.Log($"{clientName} connected!", LoggingTarget.Network);
isConnected.Value = true;
return;
}
catch (OperationCanceledException)
{
//connection process was cancelled.
throw;
}
catch (Exception e)
{
await handleErrorAndDelay(e, cancellationToken).ConfigureAwait(false);
}
}
}
finally
{
connectionLock.Release();
}
}
/// <summary>
/// Handles an exception and delays an async flow.
/// </summary>
private async Task handleErrorAndDelay(Exception exception, CancellationToken cancellationToken)
{
Logger.Log($"{clientName} connect attempt failed: {exception.Message}", LoggingTarget.Network);
await Task.Delay(5000, cancellationToken).ConfigureAwait(false);
}
private HubConnection buildConnection(CancellationToken cancellationToken)
{ {
var builder = new HubConnectionBuilder() var builder = new HubConnectionBuilder()
.WithUrl(endpoint, options => .WithUrl(endpoint, options =>
@ -188,59 +91,9 @@ namespace osu.Game.Online
ConfigureConnection?.Invoke(newConnection); ConfigureConnection?.Invoke(newConnection);
newConnection.Closed += ex => onConnectionClosed(ex, cancellationToken); return Task.FromResult((PersistentEndpointClient)new HubClient(newConnection));
return newConnection;
} }
private async Task onConnectionClosed(Exception? ex, CancellationToken cancellationToken) protected override string ClientName { get; }
{
isConnected.Value = false;
if (ex != null)
await handleErrorAndDelay(ex, cancellationToken).ConfigureAwait(false);
else
Logger.Log($"{clientName} disconnected", LoggingTarget.Network);
// make sure a disconnect wasn't triggered (and this is still the active connection).
if (!cancellationToken.IsCancellationRequested)
await Task.Run(connect, default).ConfigureAwait(false);
}
private async Task disconnect(bool takeLock)
{
cancelExistingConnect();
if (takeLock)
{
if (!await connectionLock.WaitAsync(10000).ConfigureAwait(false))
throw new TimeoutException("Could not obtain a lock to disconnect. A previous attempt is likely stuck.");
}
try
{
if (CurrentConnection != null)
await CurrentConnection.DisposeAsync().ConfigureAwait(false);
}
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

@ -0,0 +1,35 @@
// 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;
using System.Threading;
using System.Threading.Tasks;
namespace osu.Game.Online
{
public abstract class PersistentEndpointClient : IAsyncDisposable
{
/// <summary>
/// An event notifying the <see cref="PersistentEndpointClientConnector"/> that the connection has been closed
/// </summary>
public event Func<Exception?, Task>? Closed;
/// <summary>
/// Notifies the <see cref="PersistentEndpointClientConnector"/> that the connection has been closed.
/// </summary>
/// <param name="exception">The exception that the connection closed with.</param>
protected Task InvokeClosed(Exception? exception) => Closed?.Invoke(exception) ?? Task.CompletedTask;
/// <summary>
/// Connects the client to the remote service to begin processing messages.
/// </summary>
/// <param name="cancellationToken">A cancellation token to stop processing messages.</param>
public abstract Task ConnectAsync(CancellationToken cancellationToken);
public virtual ValueTask DisposeAsync()
{
Closed = null;
return new ValueTask(Task.CompletedTask);
}
}
}

View File

@ -0,0 +1,198 @@
// 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;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Bindables;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Logging;
using osu.Game.Online.API;
namespace osu.Game.Online
{
public abstract class PersistentEndpointClientConnector : IDisposable
{
/// <summary>
/// Whether the managed connection is currently connected. When <c>true</c> use <see cref="CurrentConnection"/> to access the connection.
/// </summary>
public IBindable<bool> IsConnected => isConnected;
/// <summary>
/// The current connection opened by this connector.
/// </summary>
public PersistentEndpointClient? CurrentConnection { get; private set; }
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="PersistentEndpointClientConnector"/>.
/// </summary>
/// <param name="api"> An API provider used to react to connection state changes.</param>
protected PersistentEndpointClientConnector(IAPIProvider api)
{
apiState.BindTo(api.State);
apiState.BindValueChanged(_ => Task.Run(connectIfPossible), true);
}
public Task Reconnect()
{
Logger.Log($"{ClientName} reconnecting...", LoggingTarget.Network);
return Task.Run(connectIfPossible);
}
private async Task connectIfPossible()
{
switch (apiState.Value)
{
case APIState.Failing:
case APIState.Offline:
await disconnect(true);
break;
case APIState.Online:
await connect();
break;
}
}
private async Task connect()
{
cancelExistingConnect();
if (!await connectionLock.WaitAsync(10000).ConfigureAwait(false))
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).ConfigureAwait(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 = await BuildConnectionAsync(cancellationToken).ConfigureAwait(false);
CurrentConnection.Closed += ex => onConnectionClosed(ex, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
await CurrentConnection.ConnectAsync(cancellationToken).ConfigureAwait(false);
Logger.Log($"{ClientName} connected!", LoggingTarget.Network);
isConnected.Value = true;
return;
}
catch (OperationCanceledException)
{
//connection process was cancelled.
throw;
}
catch (Exception e)
{
await handleErrorAndDelay(e, cancellationToken).ConfigureAwait(false);
}
}
}
finally
{
connectionLock.Release();
}
}
/// <summary>
/// Handles an exception and delays an async flow.
/// </summary>
private async Task handleErrorAndDelay(Exception exception, CancellationToken cancellationToken)
{
Logger.Log($"{ClientName} connect attempt failed: {exception.Message}", LoggingTarget.Network);
await Task.Delay(5000, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Creates a new <see cref="PersistentEndpointClient"/>.
/// </summary>
/// <param name="cancellationToken">A cancellation token to stop the process.</param>
protected abstract Task<PersistentEndpointClient> BuildConnectionAsync(CancellationToken cancellationToken);
private async Task onConnectionClosed(Exception? ex, CancellationToken cancellationToken)
{
isConnected.Value = false;
if (ex != null)
await handleErrorAndDelay(ex, cancellationToken).ConfigureAwait(false);
else
Logger.Log($"{ClientName} disconnected", LoggingTarget.Network);
// make sure a disconnect wasn't triggered (and this is still the active connection).
if (!cancellationToken.IsCancellationRequested)
await Task.Run(connect, default).ConfigureAwait(false);
}
private async Task disconnect(bool takeLock)
{
cancelExistingConnect();
if (takeLock)
{
if (!await connectionLock.WaitAsync(10000).ConfigureAwait(false))
throw new TimeoutException("Could not obtain a lock to disconnect. A previous attempt is likely stuck.");
}
try
{
if (CurrentConnection != null)
await CurrentConnection.DisposeAsync().ConfigureAwait(false);
}
finally
{
CurrentConnection = null;
if (takeLock)
connectionLock.Release();
}
}
private void cancelExistingConnect()
{
connectCancelSource.Cancel();
connectCancelSource = new CancellationTokenSource();
}
protected virtual string ClientName => GetType().ReadableName();
public override string ToString() => $"{ClientName} ({(IsConnected.Value ? "connected" : "not connected")})";
private bool isDisposed;
protected virtual void Dispose(bool isDisposing)
{
if (isDisposed)
return;
apiState.UnbindAll();
cancelExistingConnect();
isDisposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}

View File

@ -148,7 +148,7 @@ namespace osu.Game.Overlays.Notifications
} }
} }
private bool completionSent; private int completionSent;
/// <summary> /// <summary>
/// Attempt to post a completion notification. /// Attempt to post a completion notification.
@ -162,11 +162,11 @@ namespace osu.Game.Overlays.Notifications
if (CompletionTarget == null) if (CompletionTarget == null)
return; return;
if (completionSent) // Thread-safe barrier, as this may be called by a web request and also scheduled to the update thread at the same time.
if (Interlocked.Exchange(ref completionSent, 1) == 1)
return; return;
CompletionTarget.Invoke(CreateCompletionNotification()); CompletionTarget.Invoke(CreateCompletionNotification());
completionSent = true;
Close(false); Close(false);
} }

View File

@ -1,22 +1,27 @@
// 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 disable
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
namespace osu.Game.Screens.Play.PlayerSettings namespace osu.Game.Screens.Play.PlayerSettings
{ {
public class PlayerCheckbox : OsuCheckbox public class PlayerCheckbox : SettingsCheckbox
{ {
[BackgroundDependencyLoader] protected override Drawable CreateControl() => new PlayerCheckboxControl();
private void load(OsuColour colours)
public class PlayerCheckboxControl : OsuCheckbox
{ {
Nub.AccentColour = colours.Yellow; [BackgroundDependencyLoader]
Nub.GlowingAccentColour = colours.YellowLighter; private void load(OsuColour colours)
Nub.GlowColour = colours.YellowDark; {
Nub.AccentColour = colours.Yellow;
Nub.GlowingAccentColour = colours.YellowLighter;
Nub.GlowColour = colours.YellowDark;
}
} }
} }
} }

View File

@ -1,12 +1,9 @@
// 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 disable
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.Sprites;
using osu.Game.Localisation; using osu.Game.Localisation;
namespace osu.Game.Screens.Play.PlayerSettings namespace osu.Game.Screens.Play.PlayerSettings
@ -24,26 +21,16 @@ namespace osu.Game.Screens.Play.PlayerSettings
{ {
Children = new Drawable[] Children = new Drawable[]
{ {
new OsuSpriteText
{
Text = GameplaySettingsStrings.BackgroundDim
},
dimSliderBar = new PlayerSliderBar<double> dimSliderBar = new PlayerSliderBar<double>
{ {
LabelText = GameplaySettingsStrings.BackgroundDim,
DisplayAsPercentage = true DisplayAsPercentage = true
}, },
new OsuSpriteText
{
Text = GameplaySettingsStrings.BackgroundBlur
},
blurSliderBar = new PlayerSliderBar<double> blurSliderBar = new PlayerSliderBar<double>
{ {
LabelText = GameplaySettingsStrings.BackgroundBlur,
DisplayAsPercentage = true DisplayAsPercentage = true
}, },
new OsuSpriteText
{
Text = "Toggles:"
},
showStoryboardToggle = new PlayerCheckbox { LabelText = GraphicsSettingsStrings.StoryboardVideo }, showStoryboardToggle = new PlayerCheckbox { LabelText = GraphicsSettingsStrings.StoryboardVideo },
beatmapSkinsToggle = new PlayerCheckbox { LabelText = SkinSettingsStrings.BeatmapSkins }, beatmapSkinsToggle = new PlayerCheckbox { LabelText = SkinSettingsStrings.BeatmapSkins },
beatmapColorsToggle = new PlayerCheckbox { LabelText = SkinSettingsStrings.BeatmapColours }, beatmapColorsToggle = new PlayerCheckbox { LabelText = SkinSettingsStrings.BeatmapColours },