diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneCreateMultiplayerMatchButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneCreateMultiplayerMatchButton.cs new file mode 100644 index 0000000000..381270c5aa --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneCreateMultiplayerMatchButton.cs @@ -0,0 +1,55 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Screens.OnlinePlay.Multiplayer; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneCreateMultiplayerMatchButton : MultiplayerTestScene + { + [Cached] + private OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker(); + + private CreateMultiplayerMatchButton button; + + public override void SetUpSteps() + { + base.SetUpSteps(); + AddStep("create button", () => Child = button = new CreateMultiplayerMatchButton + { + Width = 200, + Height = 100, + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); + } + + [Test] + public void TestButtonEnableStateChanges() + { + IDisposable joiningRoomOperation = null; + + assertButtonEnableState(true); + + AddStep("begin joining room", () => joiningRoomOperation = ongoingOperationTracker.BeginOperation()); + assertButtonEnableState(false); + + AddStep("end joining room", () => joiningRoomOperation.Dispose()); + assertButtonEnableState(true); + + AddStep("disconnect client", () => Client.Disconnect()); + assertButtonEnableState(false); + + AddStep("re-connect client", () => Client.Connect()); + assertButtonEnableState(true); + } + + private void assertButtonEnableState(bool enabled) + => AddAssert($"button {(enabled ? "enabled" : "disabled")}", () => button.Enabled.Value == enabled); + } +} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 8869718fd1..2344ebea0e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -3,10 +3,12 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu; +using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Tests.Beatmaps; @@ -18,6 +20,9 @@ namespace osu.Game.Tests.Visual.Multiplayer { private MultiplayerMatchSubScreen screen; + [Cached] + private OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker(); + public TestSceneMultiplayerMatchSubScreen() : base(false) { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 03ba73d35b..878776bf51 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -30,6 +31,8 @@ namespace osu.Game.Tests.Visual.Multiplayer private BeatmapManager beatmaps; private RulesetStore rulesets; + private IDisposable readyClickOperation; + [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { @@ -56,6 +59,19 @@ namespace osu.Game.Tests.Visual.Multiplayer Beatmap = { Value = Beatmap.Value.BeatmapInfo }, Ruleset = { Value = Beatmap.Value.BeatmapInfo.Ruleset } } + }, + OnReadyClick = async () => + { + readyClickOperation = OngoingOperationTracker.BeginOperation(); + + if (Client.IsHost && Client.LocalUser?.State == MultiplayerUserState.Ready) + { + await Client.StartMatch(); + return; + } + + await Client.ToggleReady(); + readyClickOperation.Dispose(); } }; }); @@ -108,8 +124,7 @@ namespace osu.Game.Tests.Visual.Multiplayer addClickButtonStep(); AddAssert("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready); - addClickButtonStep(); - AddAssert("match started", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad); + verifyGameplayStartFlow(); } [Test] @@ -124,8 +139,7 @@ namespace osu.Game.Tests.Visual.Multiplayer addClickButtonStep(); AddStep("make user host", () => Client.TransferHost(Client.Room?.Users[0].UserID ?? 0)); - addClickButtonStep(); - AddAssert("match started", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad); + verifyGameplayStartFlow(); } [Test] @@ -179,5 +193,15 @@ namespace osu.Game.Tests.Visual.Multiplayer InputManager.MoveMouseTo(button); InputManager.Click(MouseButton.Left); }); + + private void verifyGameplayStartFlow() + { + addClickButtonStep(); + AddAssert("user waiting for load", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad); + AddAssert("ready button disabled", () => !button.ChildrenOfType().Single().Enabled.Value); + + AddStep("transitioned to gameplay", () => readyClickOperation.Dispose()); + AddAssert("ready button enabled", () => button.ChildrenOfType().Single().Enabled.Value); + } } } diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index dc80488d39..34cba09e8c 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -65,6 +65,23 @@ namespace osu.Game.Online.Multiplayer /// public readonly BindableList CurrentMatchPlayingUserIds = new BindableList(); + /// + /// The corresponding to the local player, if available. + /// + public MultiplayerRoomUser? LocalUser => Room?.Users.SingleOrDefault(u => u.User?.Id == api.LocalUser.Value.Id); + + /// + /// Whether the is the host in . + /// + public bool IsHost + { + get + { + var localUser = LocalUser; + return localUser != null && Room?.Host != null && localUser.Equals(Room.Host); + } + } + [Resolved] private UserLookupCache userLookupCache { get; set; } = null!; @@ -178,6 +195,32 @@ namespace osu.Game.Online.Multiplayer }); } + /// + /// Toggles the 's ready state. + /// + /// If a toggle of ready state is not valid at this time. + public async Task ToggleReady() + { + var localUser = LocalUser; + + if (localUser == null) + return; + + switch (localUser.State) + { + case MultiplayerUserState.Idle: + await ChangeState(MultiplayerUserState.Ready); + return; + + case MultiplayerUserState.Ready: + await ChangeState(MultiplayerUserState.Idle); + return; + + default: + throw new InvalidOperationException($"Cannot toggle ready when in {localUser.State}"); + } + } + public abstract Task TransferHost(int userId); public abstract Task ChangeSettings(MultiplayerRoomSettings settings); diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 0f06188dc2..f13d623eae 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -1,7 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Diagnostics; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -26,6 +29,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby(); private readonly IBindable initialRoomsReceived = new Bindable(); + private readonly IBindable operationInProgress = new Bindable(); private FilterControl filter; private Container content; @@ -37,7 +41,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge [Resolved] private MusicController music { get; set; } - private bool joiningRoom; + [Resolved(CanBeNull = true)] + private OngoingOperationTracker ongoingOperationTracker { get; set; } + + [CanBeNull] + private IDisposable joiningRoomOperation { get; set; } [BackgroundDependencyLoader] private void load() @@ -98,7 +106,13 @@ namespace osu.Game.Screens.OnlinePlay.Lounge base.LoadComplete(); initialRoomsReceived.BindTo(RoomManager.InitialRoomsReceived); - initialRoomsReceived.BindValueChanged(onInitialRoomsReceivedChanged, true); + initialRoomsReceived.BindValueChanged(_ => updateLoadingLayer()); + + if (ongoingOperationTracker != null) + { + operationInProgress.BindTo(ongoingOperationTracker.InProgress); + operationInProgress.BindValueChanged(_ => updateLoadingLayer(), true); + } } protected override void UpdateAfterChildren() @@ -156,26 +170,24 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private void joinRequested(Room room) { - joiningRoom = true; - updateLoadingLayer(); + Debug.Assert(joiningRoomOperation == null); + joiningRoomOperation = ongoingOperationTracker?.BeginOperation(); RoomManager?.JoinRoom(room, r => { Open(room); - joiningRoom = false; - updateLoadingLayer(); + joiningRoomOperation?.Dispose(); + joiningRoomOperation = null; }, _ => { - joiningRoom = false; - updateLoadingLayer(); + joiningRoomOperation?.Dispose(); + joiningRoomOperation = null; }); } - private void onInitialRoomsReceivedChanged(ValueChangedEvent received) => updateLoadingLayer(); - private void updateLoadingLayer() { - if (joiningRoom || !initialRoomsReceived.Value) + if (operationInProgress.Value || !initialRoomsReceived.Value) loadingLayer.Show(); else loadingLayer.Hide(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs index 163efd9c20..87b0e49b5b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs @@ -10,14 +10,34 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { public class CreateMultiplayerMatchButton : PurpleTriangleButton { + private IBindable isConnected; + private IBindable operationInProgress; + + [Resolved] + private StatefulMultiplayerClient multiplayerClient { get; set; } + + [Resolved] + private OngoingOperationTracker ongoingOperationTracker { get; set; } + [BackgroundDependencyLoader] - private void load(StatefulMultiplayerClient multiplayerClient) + private void load() { Triangles.TriangleScale = 1.5f; Text = "Create room"; - ((IBindable)Enabled).BindTo(multiplayerClient.IsConnected); + isConnected = multiplayerClient.IsConnected.GetBoundCopy(); + operationInProgress = ongoingOperationTracker.InProgress.GetBoundCopy(); } + + protected override void LoadComplete() + { + base.LoadComplete(); + + isConnected.BindValueChanged(_ => updateState()); + operationInProgress.BindValueChanged(_ => updateState(), true); + } + + private void updateState() => Enabled.Value = isConnected.Value && !operationInProgress.Value; } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs index a52f62fe00..bbf861fac3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -19,7 +20,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match public readonly Bindable SelectedItem = new Bindable(); + public Action OnReadyClick + { + set => readyButton.OnReadyClick = value; + } + private readonly Drawable background; + private readonly MultiplayerReadyButton readyButton; public MultiplayerMatchFooter() { @@ -29,7 +36,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match InternalChildren = new[] { background = new Box { RelativeSizeAxes = Axes.Both }, - new MultiplayerReadyButton + readyButton = new MultiplayerReadyButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 67c6aa7add..f0064ae0b4 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -68,6 +70,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match [Resolved] private Bindable ruleset { get; set; } + [Resolved] + private OngoingOperationTracker ongoingOperationTracker { get; set; } + + private readonly IBindable operationInProgress = new BindableBool(); + + [CanBeNull] + private IDisposable applyingSettingsOperation; + [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -265,13 +275,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match Type.BindValueChanged(type => TypePicker.Current.Value = type.NewValue, true); MaxParticipants.BindValueChanged(count => MaxParticipantsField.Text = count.NewValue?.ToString(), true); RoomID.BindValueChanged(roomId => initialBeatmapControl.Alpha = roomId.NewValue == null ? 1 : 0, true); + + operationInProgress.BindTo(ongoingOperationTracker.InProgress); + operationInProgress.BindValueChanged(v => + { + if (v.NewValue) + loadingLayer.Show(); + else + loadingLayer.Hide(); + }); } protected override void Update() { base.Update(); - ApplyButton.Enabled.Value = Playlist.Count > 0 && NameField.Text.Length > 0; + ApplyButton.Enabled.Value = Playlist.Count > 0 && NameField.Text.Length > 0 && !operationInProgress.Value; } private void apply() @@ -280,7 +299,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match return; hideError(); - loadingLayer.Show(); + + Debug.Assert(applyingSettingsOperation == null); + applyingSettingsOperation = ongoingOperationTracker.BeginOperation(); // If the client is already in a room, update via the client. // Otherwise, update the room directly in preparation for it to be submitted to the API on match creation. @@ -313,16 +334,23 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private void onSuccess(Room room) { - loadingLayer.Hide(); + Debug.Assert(applyingSettingsOperation != null); + SettingsApplied?.Invoke(); + + applyingSettingsOperation.Dispose(); + applyingSettingsOperation = null; } private void onError(string text) { + Debug.Assert(applyingSettingsOperation != null); + ErrorText.Text = text; ErrorText.FadeIn(50); - loadingLayer.Hide(); + applyingSettingsOperation.Dispose(); + applyingSettingsOperation = null; } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 281e92404c..04030cdbfd 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -1,15 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Diagnostics; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Online.API; @@ -24,15 +23,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { public Bindable SelectedItem => button.SelectedItem; + public Action OnReadyClick + { + set => button.Action = value; + } + [Resolved] private IAPIProvider api { get; set; } - [CanBeNull] - private MultiplayerRoomUser localUser; - [Resolved] private OsuColour colours { get; set; } + [Resolved] + private OngoingOperationTracker ongoingOperationTracker { get; set; } + + private IBindable operationInProgress; + private SampleChannel sampleReadyCount; private readonly ButtonWithTrianglesExposed button; @@ -46,7 +52,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match RelativeSizeAxes = Axes.Both, Size = Vector2.One, Enabled = { Value = true }, - Action = onClick }; } @@ -54,21 +59,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private void load(AudioManager audio) { sampleReadyCount = audio.Samples.Get(@"SongSelect/select-difficulty"); + + operationInProgress = ongoingOperationTracker.InProgress.GetBoundCopy(); + operationInProgress.BindValueChanged(_ => updateState()); } protected override void OnRoomUpdated() { base.OnRoomUpdated(); - // this method is called on leaving the room, so the local user may not exist in the room any more. - localUser = Room?.Users.SingleOrDefault(u => u.User?.Id == api.LocalUser.Value.Id); - - button.Enabled.Value = Client.Room?.State == MultiplayerRoomState.Open; updateState(); } private void updateState() { + var localUser = Client.LocalUser; + if (localUser == null) return; @@ -100,6 +106,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match break; } + button.Enabled.Value = Client.Room?.State == MultiplayerRoomState.Open && !operationInProgress.Value; + if (newCountReady != countReady) { countReady = newCountReady; @@ -132,22 +140,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match } } - private void onClick() - { - if (localUser == null) - return; - - if (localUser.State == MultiplayerUserState.Idle) - Client.ChangeState(MultiplayerUserState.Ready).CatchUnobservedExceptions(true); - else - { - if (Room?.Host?.Equals(localUser) == true) - Client.StartMatch().CatchUnobservedExceptions(true); - else - Client.ChangeState(MultiplayerUserState.Idle).CatchUnobservedExceptions(true); - } - } - private class ButtonWithTrianglesExposed : ReadyButton { public new Triangles Triangles => base.Triangles; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index ffa36ecfdb..e539b315e4 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -1,14 +1,17 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Specialized; using System.Diagnostics; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; +using osu.Game.Extensions; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; @@ -31,10 +34,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved] private StatefulMultiplayerClient client { get; set; } + [Resolved] + private OngoingOperationTracker ongoingOperationTracker { get; set; } + private MultiplayerMatchSettingsOverlay settingsOverlay; private IBindable isConnected; + [CanBeNull] + private IDisposable readyClickOperation; + public MultiplayerMatchSubScreen(Room room) { Title = room.RoomID.Value == null ? "New room" : room.Name.Value; @@ -150,7 +159,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer }, new Drawable[] { - new MultiplayerMatchFooter { SelectedItem = { BindTarget = SelectedItem } } + new MultiplayerMatchFooter + { + SelectedItem = { BindTarget = SelectedItem }, + OnReadyClick = onReadyClick + } } }, RowDimensions = new[] @@ -196,6 +209,44 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) => SelectedItem.Value = Playlist.FirstOrDefault(); + private void onReadyClick() + { + Debug.Assert(readyClickOperation == null); + readyClickOperation = ongoingOperationTracker.BeginOperation(); + + if (client.IsHost && client.LocalUser?.State == MultiplayerUserState.Ready) + { + client.StartMatch() + .ContinueWith(t => + { + // accessing Exception here silences any potential errors from the antecedent task + if (t.Exception != null) + { + t.CatchUnobservedExceptions(true); // will run immediately. + // gameplay was not started due to an exception; unblock button. + endOperation(); + } + + // gameplay is starting, the button will be unblocked on load requested. + }); + return; + } + + client.ToggleReady() + .ContinueWith(t => + { + t.CatchUnobservedExceptions(true); // will run immediately. + endOperation(); + }); + + void endOperation() + { + Debug.Assert(readyClickOperation != null); + readyClickOperation.Dispose(); + readyClickOperation = null; + } + } + private void onLoadRequested() { Debug.Assert(client.Room != null); @@ -203,6 +254,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer int[] userIds = client.CurrentMatchPlayingUserIds.ToArray(); StartPlay(() => new MultiplayerPlayer(SelectedItem.Value, userIds)); + + Debug.Assert(readyClickOperation != null); + + readyClickOperation.Dispose(); + readyClickOperation = null; } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs b/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs new file mode 100644 index 0000000000..c34d39136e --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs @@ -0,0 +1,52 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Bindables; + +namespace osu.Game.Screens.OnlinePlay +{ + /// + /// Utility class to track ongoing online operations' progress. + /// Can be used to disable interactivity while waiting for a response from online sources. + /// + public class OngoingOperationTracker + { + /// + /// Whether there is an online operation in progress. + /// + public IBindable InProgress => inProgress; + + private readonly Bindable inProgress = new BindableBool(); + + private LeasedBindable leasedInProgress; + + /// + /// Begins tracking a new online operation. + /// + /// + /// An that will automatically mark the operation as ended on disposal. + /// + /// An operation has already been started. + public IDisposable BeginOperation() + { + if (leasedInProgress != null) + throw new InvalidOperationException("Cannot begin operation while another is in progress."); + + leasedInProgress = inProgress.BeginLease(true); + leasedInProgress.Value = true; + + return new InvokeOnDisposal(endOperation); + } + + private void endOperation() + { + if (leasedInProgress == null) + throw new InvalidOperationException("Cannot end operation multiple times."); + + leasedInProgress.Return(); + leasedInProgress = null; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 75612516a9..b73e0a7c52 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -53,6 +53,9 @@ namespace osu.Game.Screens.OnlinePlay [Cached] private readonly Bindable currentFilter = new Bindable(new FilterCriteria()); + [Cached] + private readonly OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker(); + [Resolved(CanBeNull = true)] private MusicController music { get; set; } diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index da0e39d965..75ddb34685 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -23,6 +23,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [Cached] public Bindable Filter { get; } + [Cached] + public OngoingOperationTracker OngoingOperationTracker { get; } = new OngoingOperationTracker(); + protected override Container Content => content; private readonly TestMultiplayerRoomContainer content;