1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 20:07:25 +08:00

Merge branch 'master' into non-concurrent-sample-playback

This commit is contained in:
smoogipoo 2021-02-15 14:47:32 +09:00
commit c6ed3efa4a
20 changed files with 448 additions and 330 deletions

View File

@ -18,6 +18,7 @@ using osu.Framework.Screens;
using osu.Game.Screens.Menu;
using osu.Game.Updater;
using osu.Desktop.Windows;
using osu.Game.IO;
namespace osu.Desktop
{
@ -32,7 +33,7 @@ namespace osu.Desktop
noVersionOverlay = args?.Any(a => a == "--no-version-overlay") ?? false;
}
public override Storage GetStorageForStableInstall()
public override StableStorage GetStorageForStableInstall()
{
try
{
@ -40,7 +41,7 @@ namespace osu.Desktop
{
string stablePath = getStableInstallPath();
if (!string.IsNullOrEmpty(stablePath))
return new DesktopStorage(stablePath, desktopHost);
return new StableStorage(stablePath, desktopHost);
}
}
catch (Exception)

View File

@ -21,15 +21,17 @@ namespace osu.Game.Tests.Chat
Assert.AreEqual(36, result.Links[0].Length);
}
[TestCase(LinkAction.OpenBeatmap, "456", "https://osu.ppy.sh/beatmapsets/123#osu/456")]
[TestCase(LinkAction.OpenBeatmap, "456", "https://osu.ppy.sh/beatmapsets/123#osu/456?whatever")]
[TestCase(LinkAction.OpenBeatmap, "456", "https://osu.ppy.sh/beatmapsets/123/456")]
[TestCase(LinkAction.External, null, "https://osu.ppy.sh/beatmapsets/abc/def")]
[TestCase(LinkAction.OpenBeatmapSet, "123", "https://osu.ppy.sh/beatmapsets/123")]
[TestCase(LinkAction.OpenBeatmapSet, "123", "https://osu.ppy.sh/beatmapsets/123/whatever")]
[TestCase(LinkAction.External, null, "https://osu.ppy.sh/beatmapsets/abc")]
[TestCase(LinkAction.OpenBeatmap, "456", "https://dev.ppy.sh/beatmapsets/123#osu/456")]
[TestCase(LinkAction.OpenBeatmap, "456", "https://dev.ppy.sh/beatmapsets/123#osu/456?whatever")]
[TestCase(LinkAction.OpenBeatmap, "456", "https://dev.ppy.sh/beatmapsets/123/456")]
[TestCase(LinkAction.External, null, "https://dev.ppy.sh/beatmapsets/abc/def")]
[TestCase(LinkAction.OpenBeatmapSet, "123", "https://dev.ppy.sh/beatmapsets/123")]
[TestCase(LinkAction.OpenBeatmapSet, "123", "https://dev.ppy.sh/beatmapsets/123/whatever")]
[TestCase(LinkAction.External, null, "https://dev.ppy.sh/beatmapsets/abc")]
public void TestBeatmapLinks(LinkAction expectedAction, string expectedArg, string link)
{
MessageFormatter.WebsiteRootUrl = "dev.ppy.sh";
Message result = MessageFormatter.FormatMessage(new Message { Content = link });
Assert.AreEqual(result.Content, result.DisplayContent);

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

@ -13,15 +13,18 @@ namespace osu.Game.Tournament.Tests
{
base.LoadComplete();
LoadComponentAsync(new Background("Menu/menu-background-0")
BracketLoadTask.ContinueWith(_ => Schedule(() =>
{
Colour = OsuColour.Gray(0.5f),
Depth = 10
}, AddInternal);
LoadComponentAsync(new Background("Menu/menu-background-0")
{
Colour = OsuColour.Gray(0.5f),
Depth = 10
}, AddInternal);
// Have to construct this here, rather than in the constructor, because
// we depend on some dependencies to be loaded within OsuGameBase.load().
Add(new TestBrowser());
// Have to construct this here, rather than in the constructor, because
// we depend on some dependencies to be loaded within OsuGameBase.load().
Add(new TestBrowser());
}));
}
}
}

View File

@ -154,10 +154,13 @@ namespace osu.Game.Tournament.Tests
protected override void LoadAsyncComplete()
{
// this has to be run here rather than LoadComplete because
// TestScene.cs is checking the IsLoaded state (on another thread) and expects
// the runner to be loaded at that point.
Add(runner = new TestSceneTestRunner.TestRunner());
BracketLoadTask.ContinueWith(_ => Schedule(() =>
{
// this has to be run here rather than LoadComplete because
// TestScene.cs is checking the IsLoaded state (on another thread) and expects
// the runner to be loaded at that point.
Add(runner = new TestSceneTestRunner.TestRunner());
}));
}
public void RunTestBlocking(TestScene test) => runner.RunTestBlocking(test);

View File

@ -13,6 +13,7 @@ using osu.Framework.Graphics.Colour;
using osu.Game.Graphics.Cursor;
using osu.Game.Tournament.Models;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
@ -32,25 +33,24 @@ namespace osu.Game.Tournament
private Drawable heightWarning;
private Bindable<Size> windowSize;
private Bindable<WindowMode> windowMode;
private LoadingSpinner loadingSpinner;
[BackgroundDependencyLoader]
private void load(FrameworkConfigManager frameworkConfig)
{
windowSize = frameworkConfig.GetBindable<Size>(FrameworkSetting.WindowedSize);
windowSize.BindValueChanged(size => ScheduleAfterChildren(() =>
{
var minWidth = (int)(size.NewValue.Height / 768f * TournamentSceneManager.REQUIRED_WIDTH) - 1;
heightWarning.Alpha = size.NewValue.Width < minWidth ? 1 : 0;
}), true);
windowMode = frameworkConfig.GetBindable<WindowMode>(FrameworkSetting.WindowMode);
windowMode.BindValueChanged(mode => ScheduleAfterChildren(() =>
{
windowMode.Value = WindowMode.Windowed;
}), true);
AddRange(new[]
Add(loadingSpinner = new LoadingSpinner(true, true)
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Margin = new MarginPadding(40),
});
loadingSpinner.Show();
BracketLoadTask.ContinueWith(_ => LoadComponentsAsync(new[]
{
new Container
{
@ -93,7 +93,24 @@ namespace osu.Game.Tournament
RelativeSizeAxes = Axes.Both,
Child = new TournamentSceneManager()
}
});
}, drawables =>
{
loadingSpinner.Hide();
loadingSpinner.Expire();
AddRange(drawables);
windowSize.BindValueChanged(size => ScheduleAfterChildren(() =>
{
var minWidth = (int)(size.NewValue.Height / 768f * TournamentSceneManager.REQUIRED_WIDTH) - 1;
heightWarning.Alpha = size.NewValue.Width < minWidth ? 1 : 0;
}), true);
windowMode.BindValueChanged(mode => ScheduleAfterChildren(() =>
{
windowMode.Value = WindowMode.Windowed;
}), true);
}));
}
}
}

View File

@ -4,6 +4,7 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Textures;
@ -29,6 +30,10 @@ namespace osu.Game.Tournament
private DependencyContainer dependencies;
private FileBasedIPC ipc;
protected Task BracketLoadTask => taskCompletionSource.Task;
private readonly TaskCompletionSource<bool> taskCompletionSource = new TaskCompletionSource<bool>();
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
return dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
@ -46,14 +51,9 @@ namespace osu.Game.Tournament
Textures.AddStore(new TextureLoaderStore(new StorageBackedResourceStore(storage)));
readBracket();
ladder.CurrentMatch.Value = ladder.Matches.FirstOrDefault(p => p.Current.Value);
dependencies.CacheAs(new StableInfo(storage));
dependencies.CacheAs<MatchIPCInfo>(ipc = new FileBasedIPC());
Add(ipc);
Task.Run(readBracket);
}
private void readBracket()
@ -68,10 +68,6 @@ namespace osu.Game.Tournament
ladder ??= new LadderInfo();
ladder.Ruleset.Value ??= RulesetStore.AvailableRulesets.First();
Ruleset.BindTo(ladder.Ruleset);
dependencies.Cache(ladder);
bool addedInfo = false;
// assign teams
@ -127,6 +123,19 @@ namespace osu.Game.Tournament
if (addedInfo)
SaveChanges();
ladder.CurrentMatch.Value = ladder.Matches.FirstOrDefault(p => p.Current.Value);
Schedule(() =>
{
Ruleset.BindTo(ladder.Ruleset);
dependencies.Cache(ladder);
dependencies.CacheAs<MatchIPCInfo>(ipc = new FileBasedIPC());
Add(ipc);
taskCompletionSource.SetResult(true);
});
}
/// <summary>

View File

@ -64,7 +64,9 @@ namespace osu.Game.Beatmaps
protected override string[] HashableFileTypes => new[] { ".osu" };
protected override string ImportFromStablePath => "Songs";
protected override string ImportFromStablePath => ".";
protected override Storage PrepareStableStorage(StableStorage stableStorage) => stableStorage.GetSongStorage();
private readonly RulesetStore rulesets;
private readonly BeatmapStore beatmaps;

View File

@ -625,7 +625,7 @@ namespace osu.Game.Database
/// <summary>
/// Set a storage with access to an osu-stable install for import purposes.
/// </summary>
public Func<Storage> GetStableStorage { private get; set; }
public Func<StableStorage> GetStableStorage { private get; set; }
/// <summary>
/// Denotes whether an osu-stable installation is present to perform automated imports from.
@ -638,9 +638,10 @@ namespace osu.Game.Database
protected virtual string ImportFromStablePath => null;
/// <summary>
/// Select paths to import from stable. Default implementation iterates all directories in <see cref="ImportFromStablePath"/>.
/// Select paths to import from stable where all paths should be absolute. Default implementation iterates all directories in <see cref="ImportFromStablePath"/>.
/// </summary>
protected virtual IEnumerable<string> GetStableImportPaths(Storage stableStoage) => stableStoage.GetDirectories(ImportFromStablePath);
protected virtual IEnumerable<string> GetStableImportPaths(Storage storage) => storage.GetDirectories(ImportFromStablePath)
.Select(path => storage.GetFullPath(path));
/// <summary>
/// Whether this specified path should be removed after successful import.
@ -654,24 +655,33 @@ namespace osu.Game.Database
/// </summary>
public Task ImportFromStableAsync()
{
var stable = GetStableStorage?.Invoke();
var stableStorage = GetStableStorage?.Invoke();
if (stable == null)
if (stableStorage == null)
{
Logger.Log("No osu!stable installation available!", LoggingTarget.Information, LogLevel.Error);
return Task.CompletedTask;
}
if (!stable.ExistsDirectory(ImportFromStablePath))
var storage = PrepareStableStorage(stableStorage);
if (!storage.ExistsDirectory(ImportFromStablePath))
{
// This handles situations like when the user does not have a Skins folder
Logger.Log($"No {ImportFromStablePath} folder available in osu!stable installation", LoggingTarget.Information, LogLevel.Error);
return Task.CompletedTask;
}
return Task.Run(async () => await Import(GetStableImportPaths(GetStableStorage()).Select(f => stable.GetFullPath(f)).ToArray()));
return Task.Run(async () => await Import(GetStableImportPaths(storage).ToArray()));
}
/// <summary>
/// Run any required traversal operations on the stable storage location before performing operations.
/// </summary>
/// <param name="stableStorage">The stable storage.</param>
/// <returns>The usable storage. Return the unchanged <paramref name="stableStorage"/> if no traversal is required.</returns>
protected virtual Storage PrepareStableStorage(StableStorage stableStorage) => stableStorage;
#endregion
/// <summary>

View File

@ -0,0 +1,62 @@
// 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.IO;
using System.Linq;
using osu.Framework.Platform;
namespace osu.Game.IO
{
/// <summary>
/// A storage pointing to an osu-stable installation.
/// Provides methods for handling installations with a custom Song folder location.
/// </summary>
public class StableStorage : DesktopStorage
{
private const string stable_default_songs_path = "Songs";
private readonly DesktopGameHost host;
private readonly Lazy<string> songsPath;
public StableStorage(string path, DesktopGameHost host)
: base(path, host)
{
this.host = host;
songsPath = new Lazy<string>(locateSongsDirectory);
}
/// <summary>
/// Returns a <see cref="Storage"/> pointing to the osu-stable Songs directory.
/// </summary>
public Storage GetSongStorage() => new DesktopStorage(songsPath.Value, host);
private string locateSongsDirectory()
{
var configFile = GetFiles(".", $"osu!.{Environment.UserName}.cfg").SingleOrDefault();
if (configFile != null)
{
using (var stream = GetStream(configFile))
using (var textReader = new StreamReader(stream))
{
string line;
while ((line = textReader.ReadLine()) != null)
{
if (!line.StartsWith("BeatmapDirectory", StringComparison.OrdinalIgnoreCase)) continue;
var customDirectory = line.Split('=').LastOrDefault()?.Trim();
if (customDirectory != null && Path.IsPathFullyQualified(customDirectory))
return customDirectory;
break;
}
}
}
return GetFullPath(stable_default_songs_path);
}
}
}

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;
}

View File

@ -28,7 +28,6 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Collections;
@ -52,6 +51,7 @@ using osu.Game.Updater;
using osu.Game.Utils;
using LogLevel = osu.Framework.Logging.LogLevel;
using osu.Game.Database;
using osu.Game.IO;
namespace osu.Game
{
@ -88,7 +88,7 @@ namespace osu.Game
protected SentryLogger SentryLogger;
public virtual Storage GetStorageForStableInstall() => null;
public virtual StableStorage GetStorageForStableInstall() => null;
public float ToolbarOffset => (Toolbar?.Position.Y ?? 0) + (Toolbar?.DrawHeight ?? 0);

View File

@ -52,6 +52,7 @@ namespace osu.Game.Overlays.Mods
new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
Child = modSettingsContent = new FillFlowContainer<ModControlSection>
{
Anchor = Anchor.TopCentre,

View File

@ -71,8 +71,9 @@ namespace osu.Game.Scoring
}
}
protected override IEnumerable<string> GetStableImportPaths(Storage stableStorage)
=> stableStorage.GetFiles(ImportFromStablePath).Where(p => HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false));
protected override IEnumerable<string> GetStableImportPaths(Storage storage)
=> storage.GetFiles(ImportFromStablePath).Where(p => HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false))
.Select(path => storage.GetFullPath(path));
public Score GetScore(ScoreInfo score) => new LegacyDatabasedScore(score, rulesets, beatmaps(), Files.Store);

View File

@ -68,6 +68,10 @@ namespace osu.Game.Screens.Menu
{
StartTrack();
// this classic intro loops forever.
if (UsingThemedIntro)
Track.Looping = true;
const float fade_in_time = 200;
logo.ScaleTo(1);

View File

@ -77,7 +77,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{
Horizontal = 105,
Horizontal = HORIZONTAL_OVERFLOW_PADDING + 55,
Vertical = 20
},
Child = new GridContainer
@ -237,6 +237,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Both,
Height = 0.5f,
Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING },
Child = userModsSelectOverlay = new UserModSelectOverlay
{
SelectedMods = { BindTarget = UserMods },