1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 10:52:53 +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:
Bartłomiej Dach 2023-12-05 13:51:00 +01:00 committed by GitHub
commit 88095aaefa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 164 additions and 35 deletions

View File

@ -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);

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

View File

@ -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>

View File

@ -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>

View File

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

View File

@ -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)

View File

@ -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()
}
};
}

View File

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

View File

@ -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)

View File

@ -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();
}
}

View File

@ -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);