diff --git a/Directory.Build.props b/Directory.Build.props
index 049db816a8..2e1873a9ed 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -18,7 +18,7 @@
-
+
$(MSBuildThisFileDirectory)CodeAnalysis\osu.ruleset
@@ -28,9 +28,17 @@
$(NoWarn);CS1591
-
- $(NoWarn);NU1701
+
+ $(NoWarn);NU1701;CA9998
false
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneCreateMultiplayerMatchButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneCreateMultiplayerMatchButton.cs
new file mode 100644
index 0000000000..2f0398c6ef
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneCreateMultiplayerMatchButton.cs
@@ -0,0 +1,50 @@
+// 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.Graphics;
+using osu.Game.Screens.OnlinePlay.Multiplayer;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestSceneCreateMultiplayerMatchButton : MultiplayerTestScene
+ {
+ 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.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
index 6f083f4ab6..0d0acbb8f4 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
@@ -131,6 +131,18 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("ensure mods not selected", () => modDisplay.Current.Value.Count == 0);
}
+ [Test]
+ public void TestExternallySetCustomizedMod()
+ {
+ AddStep("set customized mod externally", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } });
+
+ AddAssert("ensure button is selected and customized accordingly", () =>
+ {
+ var button = modSelect.GetModButton(SelectedMods.Value.Single());
+ return ((OsuModDoubleTime)button.SelectedMod).SpeedChange.Value == 1.01;
+ });
+ }
+
private void testSingleMod(Mod mod)
{
selectNext(mod);
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/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs
index 0107f94dcf..573d1e5355 100644
--- a/osu.Game/Overlays/Mods/ModSection.cs
+++ b/osu.Game/Overlays/Mods/ModSection.cs
@@ -127,20 +127,30 @@ namespace osu.Game.Overlays.Mods
}
///
- /// Select one or more mods in this section and deselects all other ones.
+ /// Updates all buttons with the given list of selected mods.
///
- /// The types of s which should be selected.
- public void SelectTypes(IEnumerable modTypes)
+ /// The new list of selected mods to select.
+ public void UpdateSelectedMods(IReadOnlyList newSelectedMods)
{
foreach (var button in buttons)
- {
- int i = Array.FindIndex(button.Mods, m => modTypes.Any(t => t == m.GetType()));
+ updateButtonMods(button, newSelectedMods);
+ }
- if (i >= 0)
- button.SelectAt(i);
- else
- button.Deselect();
+ private void updateButtonMods(ModButton button, IReadOnlyList newSelectedMods)
+ {
+ foreach (var mod in newSelectedMods)
+ {
+ var index = Array.FindIndex(button.Mods, m1 => mod.GetType() == m1.GetType());
+ if (index < 0)
+ continue;
+
+ var buttonMod = button.Mods[index];
+ buttonMod.CopyFrom(mod);
+ button.SelectAt(index);
+ return;
}
+
+ button.Deselect();
}
protected ModSection()
diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
index 491052fa2c..0c8245bebe 100644
--- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs
+++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
@@ -409,7 +409,7 @@ namespace osu.Game.Overlays.Mods
private void selectedModsChanged(ValueChangedEvent> mods)
{
foreach (var section in ModSectionsContainer.Children)
- section.SelectTypes(mods.NewValue.Select(m => m.GetType()).ToList());
+ section.UpdateSelectedMods(mods.NewValue);
updateMods();
}
diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs
index 24d184e531..3a8717e678 100644
--- a/osu.Game/Rulesets/Mods/Mod.cs
+++ b/osu.Game/Rulesets/Mods/Mod.cs
@@ -128,20 +128,29 @@ namespace osu.Game.Rulesets.Mods
///
public virtual Mod CreateCopy()
{
- var copy = (Mod)Activator.CreateInstance(GetType());
+ var result = (Mod)Activator.CreateInstance(GetType());
+ result.CopyFrom(this);
+ return result;
+ }
+
+ ///
+ /// Copies mod setting values from into this instance.
+ ///
+ /// The mod to copy properties from.
+ public void CopyFrom(Mod source)
+ {
+ if (source.GetType() != GetType())
+ throw new ArgumentException($"Expected mod of type {GetType()}, got {source.GetType()}.", nameof(source));
- // Copy bindable values across
foreach (var (_, prop) in this.GetSettingsSourceProperties())
{
- var origBindable = (IBindable)prop.GetValue(this);
- var copyBindable = (IBindable)prop.GetValue(copy);
+ var targetBindable = (IBindable)prop.GetValue(this);
+ var sourceBindable = (IBindable)prop.GetValue(source);
// we only care about changes that have been made away from defaults.
- if (!origBindable.IsDefault)
- copy.CopyAdjustedSetting(copyBindable, origBindable);
+ if (!sourceBindable.IsDefault)
+ CopyAdjustedSetting(targetBindable, sourceBindable);
}
-
- return copy;
}
///
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..5c9e9ce90b
--- /dev/null
+++ b/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs
@@ -0,0 +1,59 @@
+// 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.Graphics;
+
+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 : Component
+ {
+ ///
+ /// Whether there is an online operation in progress.
+ ///
+ public IBindable InProgress => inProgress;
+
+ private readonly Bindable inProgress = new BindableBool();
+
+ private LeasedBindable leasedInProgress;
+
+ public OngoingOperationTracker()
+ {
+ AlwaysPresent = true;
+ }
+
+ ///
+ /// 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;
+
+ // for extra safety, marshal the end of operation back to the update thread if necessary.
+ return new InvokeOnDisposal(() => Scheduler.Add(endOperation, false));
+ }
+
+ 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..71fd0d5c76 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 OngoingOperationTracker ongoingOperationTracker { get; set; }
+
[Resolved(CanBeNull = true)]
private MusicController music { get; set; }
@@ -141,7 +144,8 @@ namespace osu.Game.Screens.OnlinePlay
};
button.Action = () => OpenNewRoom();
}),
- RoomManager = CreateRoomManager()
+ RoomManager = CreateRoomManager(),
+ ongoingOperationTracker = new OngoingOperationTracker()
}
};
diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs
index e1a29946f4..9b716b323d 100644
--- a/osu.Game/Screens/OsuScreen.cs
+++ b/osu.Game/Screens/OsuScreen.cs
@@ -143,7 +143,11 @@ namespace osu.Game.Screens
private void load(OsuGame osu, AudioManager audio)
{
sampleExit = audio.Samples.Get(@"UI/screen-back");
+ }
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
Activity.Value ??= InitialActivity;
}
diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs
index da0e39d965..a87b22affe 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; }
+
protected override Container Content => content;
private readonly TestMultiplayerRoomContainer content;
@@ -36,6 +39,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
Client = content.Client;
RoomManager = content.RoomManager;
Filter = content.Filter;
+ OngoingOperationTracker = content.OngoingOperationTracker;
}
[SetUp]
diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs
index ad3e2f7105..860caef071 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs
@@ -25,6 +25,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Cached]
public readonly Bindable Filter = new Bindable(new FilterCriteria());
+ [Cached]
+ public readonly OngoingOperationTracker OngoingOperationTracker;
+
public TestMultiplayerRoomContainer()
{
RelativeSizeAxes = Axes.Both;
@@ -33,6 +36,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
Client = new TestMultiplayerClient(),
RoomManager = new TestMultiplayerRoomManager(),
+ OngoingOperationTracker = new OngoingOperationTracker(),
content = new Container { RelativeSizeAxes = Axes.Both }
});
}