mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 13:22:55 +08:00
Merge pull request #25637 from smoogipoo/multiplayer-abort
Add ability for the host to abort an in-progress match
This commit is contained in:
commit
88095aaefa
@ -378,6 +378,41 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
}, users);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAbortMatch()
|
||||
{
|
||||
AddStep("setup client", () =>
|
||||
{
|
||||
multiplayerClient.Setup(m => m.StartMatch())
|
||||
.Callback(() =>
|
||||
{
|
||||
multiplayerClient.Raise(m => m.LoadRequested -= null);
|
||||
multiplayerClient.Object.Room!.State = MultiplayerRoomState.WaitingForLoad;
|
||||
|
||||
// The local user state doesn't really matter, so let's do the same as the base implementation for these tests.
|
||||
changeUserState(localUser.UserID, MultiplayerUserState.Idle);
|
||||
});
|
||||
|
||||
multiplayerClient.Setup(m => m.AbortMatch())
|
||||
.Callback(() =>
|
||||
{
|
||||
multiplayerClient.Object.Room!.State = MultiplayerRoomState.Open;
|
||||
raiseRoomUpdated();
|
||||
});
|
||||
});
|
||||
|
||||
// Ready
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
|
||||
// Start match
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
AddUntilStep("countdown button disabled", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
|
||||
|
||||
// Abort
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
AddStep("check abort request received", () => multiplayerClient.Verify(m => m.AbortMatch(), Times.Once));
|
||||
}
|
||||
|
||||
private void verifyGameplayStartFlow()
|
||||
{
|
||||
checkLocalUserState(MultiplayerUserState.Ready);
|
||||
|
11
osu.Game/Online/Multiplayer/GameplayAbortReason.cs
Normal file
11
osu.Game/Online/Multiplayer/GameplayAbortReason.cs
Normal file
@ -0,0 +1,11 @@
|
||||
// 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.
|
||||
|
||||
namespace osu.Game.Online.Multiplayer
|
||||
{
|
||||
public enum GameplayAbortReason
|
||||
{
|
||||
LoadTookTooLong,
|
||||
HostAbortedTheMatch
|
||||
}
|
||||
}
|
@ -107,17 +107,18 @@ namespace osu.Game.Online.Multiplayer
|
||||
/// </summary>
|
||||
Task LoadRequested();
|
||||
|
||||
/// <summary>
|
||||
/// Signals that loading of gameplay is to be aborted.
|
||||
/// </summary>
|
||||
Task LoadAborted();
|
||||
|
||||
/// <summary>
|
||||
/// Signals that gameplay has started.
|
||||
/// All users in the <see cref="MultiplayerUserState.Loaded"/> or <see cref="MultiplayerUserState.ReadyForGameplay"/> states should begin gameplay as soon as possible.
|
||||
/// </summary>
|
||||
Task GameplayStarted();
|
||||
|
||||
/// <summary>
|
||||
/// Signals that gameplay has been aborted.
|
||||
/// </summary>
|
||||
/// <param name="reason">The reason why gameplay was aborted.</param>
|
||||
Task GameplayAborted(GameplayAbortReason reason);
|
||||
|
||||
/// <summary>
|
||||
/// Signals that the match has ended, all players have finished and results are ready to be displayed.
|
||||
/// </summary>
|
||||
|
@ -77,6 +77,11 @@ namespace osu.Game.Online.Multiplayer
|
||||
/// <exception cref="InvalidStateException">If an attempt to start the game occurs when the game's (or users') state disallows it.</exception>
|
||||
Task StartMatch();
|
||||
|
||||
/// <summary>
|
||||
/// As the host of a room, aborts an on-going match.
|
||||
/// </summary>
|
||||
Task AbortMatch();
|
||||
|
||||
/// <summary>
|
||||
/// Aborts an ongoing gameplay load.
|
||||
/// </summary>
|
||||
|
@ -73,9 +73,9 @@ namespace osu.Game.Online.Multiplayer
|
||||
public virtual event Action? LoadRequested;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the multiplayer server requests loading of play to be aborted.
|
||||
/// Invoked when the multiplayer server requests gameplay to be aborted.
|
||||
/// </summary>
|
||||
public event Action? LoadAborted;
|
||||
public event Action<GameplayAbortReason>? GameplayAborted;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the multiplayer server requests gameplay to be started.
|
||||
@ -374,6 +374,8 @@ namespace osu.Game.Online.Multiplayer
|
||||
|
||||
public abstract Task AbortGameplay();
|
||||
|
||||
public abstract Task AbortMatch();
|
||||
|
||||
public abstract Task AddPlaylistItem(MultiplayerPlaylistItem item);
|
||||
|
||||
public abstract Task EditPlaylistItem(MultiplayerPlaylistItem item);
|
||||
@ -682,14 +684,14 @@ namespace osu.Game.Online.Multiplayer
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Task IMultiplayerClient.LoadAborted()
|
||||
Task IMultiplayerClient.GameplayAborted(GameplayAbortReason reason)
|
||||
{
|
||||
Scheduler.Add(() =>
|
||||
{
|
||||
if (Room == null)
|
||||
return;
|
||||
|
||||
LoadAborted?.Invoke();
|
||||
GameplayAborted?.Invoke(reason);
|
||||
}, false);
|
||||
|
||||
return Task.CompletedTask;
|
||||
|
@ -58,7 +58,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
connection.On<int, MultiplayerUserState>(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged);
|
||||
connection.On(nameof(IMultiplayerClient.LoadRequested), ((IMultiplayerClient)this).LoadRequested);
|
||||
connection.On(nameof(IMultiplayerClient.GameplayStarted), ((IMultiplayerClient)this).GameplayStarted);
|
||||
connection.On(nameof(IMultiplayerClient.LoadAborted), ((IMultiplayerClient)this).LoadAborted);
|
||||
connection.On<GameplayAbortReason>(nameof(IMultiplayerClient.GameplayAborted), ((IMultiplayerClient)this).GameplayAborted);
|
||||
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);
|
||||
@ -226,6 +226,16 @@ namespace osu.Game.Online.Multiplayer
|
||||
return connection.InvokeAsync(nameof(IMultiplayerServer.AbortGameplay));
|
||||
}
|
||||
|
||||
public override Task AbortMatch()
|
||||
{
|
||||
if (!IsConnected.Value)
|
||||
return Task.CompletedTask;
|
||||
|
||||
Debug.Assert(connection != null);
|
||||
|
||||
return connection.InvokeAsync(nameof(IMultiplayerServer.AbortMatch));
|
||||
}
|
||||
|
||||
public override Task AddPlaylistItem(MultiplayerPlaylistItem item)
|
||||
{
|
||||
if (!IsConnected.Value)
|
||||
|
@ -23,6 +23,11 @@ namespace osu.Game.Overlays.Dialog
|
||||
/// </summary>
|
||||
protected Action? DangerousAction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The action to perform if cancelled.
|
||||
/// </summary>
|
||||
protected Action? CancelAction { get; set; }
|
||||
|
||||
protected DangerousActionDialog()
|
||||
{
|
||||
HeaderText = DeleteConfirmationDialogStrings.HeaderText;
|
||||
@ -38,7 +43,8 @@ namespace osu.Game.Overlays.Dialog
|
||||
},
|
||||
new PopupDialogCancelButton
|
||||
{
|
||||
Text = DeleteConfirmationDialogStrings.Cancel
|
||||
Text = DeleteConfirmationDialogStrings.Cancel,
|
||||
Action = () => CancelAction?.Invoke()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -16,6 +16,8 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.Countdown;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
@ -28,6 +30,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
[CanBeNull]
|
||||
private IDisposable clickOperation;
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private IDialogOverlay dialogOverlay { get; set; }
|
||||
|
||||
private Sample sampleReady;
|
||||
private Sample sampleReadyAll;
|
||||
private Sample sampleUnready;
|
||||
@ -56,7 +61,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = Vector2.One,
|
||||
Action = onReadyClick,
|
||||
Action = onReadyButtonClick,
|
||||
},
|
||||
countdownButton = new MultiplayerCountdownButton
|
||||
{
|
||||
@ -101,7 +106,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
endOperation();
|
||||
}
|
||||
|
||||
private void onReadyClick()
|
||||
private void onReadyButtonClick()
|
||||
{
|
||||
if (Room == null)
|
||||
return;
|
||||
@ -109,9 +114,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
Debug.Assert(clickOperation == null);
|
||||
clickOperation = ongoingOperationTracker.BeginOperation();
|
||||
|
||||
if (isReady() && Client.IsHost && !Room.ActiveCountdowns.Any(c => c is MatchStartCountdown))
|
||||
startMatch();
|
||||
else
|
||||
if (Client.IsHost)
|
||||
{
|
||||
if (Room.State == MultiplayerRoomState.Open)
|
||||
{
|
||||
if (isReady() && !Room.ActiveCountdowns.Any(c => c is MatchStartCountdown))
|
||||
startMatch();
|
||||
else
|
||||
toggleReady();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (dialogOverlay == null)
|
||||
abortMatch();
|
||||
else
|
||||
dialogOverlay.Push(new ConfirmAbortDialog(abortMatch, endOperation));
|
||||
}
|
||||
}
|
||||
else if (Room.State != MultiplayerRoomState.Closed)
|
||||
toggleReady();
|
||||
|
||||
bool isReady() => Client.LocalUser?.State == MultiplayerUserState.Ready || Client.LocalUser?.State == MultiplayerUserState.Spectating;
|
||||
@ -128,6 +148,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
// gameplay was not started due to an exception; unblock button.
|
||||
endOperation();
|
||||
});
|
||||
|
||||
void abortMatch() => Client.AbortMatch().FireAndForget(endOperation, _ => endOperation());
|
||||
}
|
||||
|
||||
private void startCountdown(TimeSpan duration)
|
||||
@ -189,7 +211,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
}
|
||||
|
||||
readyButton.Enabled.Value = countdownButton.Enabled.Value =
|
||||
Room.State == MultiplayerRoomState.Open
|
||||
Room.State != MultiplayerRoomState.Closed
|
||||
&& CurrentPlaylistItem.Value?.ID == Room.Settings.PlaylistItemId
|
||||
&& !Room.Playlist.Single(i => i.ID == Room.Settings.PlaylistItemId).Expired
|
||||
&& !operationInProgress.Value;
|
||||
@ -198,6 +220,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
if (localUser?.State == MultiplayerUserState.Spectating)
|
||||
readyButton.Enabled.Value &= Client.IsHost && newCountReady > 0 && !Room.ActiveCountdowns.Any(c => c is MatchStartCountdown);
|
||||
|
||||
// When the local user is not the host, the button should only be enabled when no match is in progress.
|
||||
if (!Client.IsHost)
|
||||
readyButton.Enabled.Value &= Room.State == MultiplayerRoomState.Open;
|
||||
|
||||
// At all times, the countdown button should only be enabled when no match is in progress.
|
||||
countdownButton.Enabled.Value &= Room.State == MultiplayerRoomState.Open;
|
||||
|
||||
if (newCountReady == countReady)
|
||||
return;
|
||||
|
||||
@ -219,5 +248,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
countReady = newCountReady;
|
||||
});
|
||||
}
|
||||
|
||||
public partial class ConfirmAbortDialog : DangerousActionDialog
|
||||
{
|
||||
public ConfirmAbortDialog(Action abortMatch, Action cancel)
|
||||
{
|
||||
HeaderText = "Are you sure you want to abort the match?";
|
||||
|
||||
DangerousAction = abortMatch;
|
||||
CancelAction = cancel;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -149,16 +149,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
{
|
||||
switch (localUser?.State)
|
||||
{
|
||||
default:
|
||||
Text = "Ready";
|
||||
break;
|
||||
|
||||
case MultiplayerUserState.Spectating:
|
||||
case MultiplayerUserState.Ready:
|
||||
Text = room.Host?.Equals(localUser) == true
|
||||
Text = multiplayerClient.IsHost
|
||||
? $"Start match {countText}"
|
||||
: $"Waiting for host... {countText}";
|
||||
break;
|
||||
|
||||
default:
|
||||
// Show the abort button for the host as long as gameplay is in progress.
|
||||
if (multiplayerClient.IsHost && room.State != MultiplayerRoomState.Open)
|
||||
Text = "Abort the match";
|
||||
else
|
||||
Text = "Ready";
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -193,12 +196,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
switch (localUser?.State)
|
||||
{
|
||||
default:
|
||||
setGreen();
|
||||
// Show the abort button for the host as long as gameplay is in progress.
|
||||
if (multiplayerClient.IsHost && room.State != MultiplayerRoomState.Open)
|
||||
setRed();
|
||||
else
|
||||
setGreen();
|
||||
break;
|
||||
|
||||
case MultiplayerUserState.Spectating:
|
||||
case MultiplayerUserState.Ready:
|
||||
if (room?.Host?.Equals(localUser) == true && !room.ActiveCountdowns.Any(c => c is MatchStartCountdown))
|
||||
if (multiplayerClient.IsHost && !room.ActiveCountdowns.Any(c => c is MatchStartCountdown))
|
||||
setGreen();
|
||||
else
|
||||
setYellow();
|
||||
@ -206,15 +213,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
break;
|
||||
}
|
||||
|
||||
void setYellow()
|
||||
{
|
||||
BackgroundColour = colours.YellowDark;
|
||||
}
|
||||
void setYellow() => BackgroundColour = colours.YellowDark;
|
||||
|
||||
void setGreen()
|
||||
{
|
||||
BackgroundColour = colours.Green;
|
||||
}
|
||||
void setGreen() => BackgroundColour = colours.Green;
|
||||
|
||||
void setRed() => BackgroundColour = colours.Red;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
|
@ -23,7 +23,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
base.LoadComplete();
|
||||
|
||||
client.RoomUpdated += onRoomUpdated;
|
||||
client.LoadAborted += onLoadAborted;
|
||||
client.GameplayAborted += onGameplayAborted;
|
||||
onRoomUpdated();
|
||||
}
|
||||
|
||||
@ -39,12 +39,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
transitionFromResults();
|
||||
}
|
||||
|
||||
private void onLoadAborted()
|
||||
private void onGameplayAborted(GameplayAbortReason reason)
|
||||
{
|
||||
// If the server aborts gameplay for this user (due to loading too slow), exit gameplay screens.
|
||||
if (!this.IsCurrentScreen())
|
||||
{
|
||||
Logger.Log("Gameplay aborted because loading the beatmap took too long.", LoggingTarget.Runtime, LogLevel.Important);
|
||||
switch (reason)
|
||||
{
|
||||
case GameplayAbortReason.LoadTookTooLong:
|
||||
Logger.Log("Gameplay aborted because loading the beatmap took too long.", LoggingTarget.Runtime, LogLevel.Important);
|
||||
break;
|
||||
|
||||
case GameplayAbortReason.HostAbortedTheMatch:
|
||||
Logger.Log("The host aborted the match.", LoggingTarget.Runtime, LogLevel.Important);
|
||||
break;
|
||||
}
|
||||
|
||||
this.MakeCurrent();
|
||||
}
|
||||
}
|
||||
|
@ -396,6 +396,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override async Task AbortMatch()
|
||||
{
|
||||
ChangeUserState(api.LocalUser.Value.Id, MultiplayerUserState.Idle);
|
||||
await ((IMultiplayerClient)this).GameplayAborted(GameplayAbortReason.HostAbortedTheMatch).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task AddUserPlaylistItem(int userId, MultiplayerPlaylistItem item)
|
||||
{
|
||||
Debug.Assert(ServerRoom != null);
|
||||
|
Loading…
Reference in New Issue
Block a user