1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 13:37:25 +08:00

Add support for starting/stopping countdowns

This commit is contained in:
Dan Balasescu 2022-03-17 19:26:42 +09:00
parent 3b938865a1
commit 72843a6797
5 changed files with 354 additions and 39 deletions

View File

@ -1,6 +1,7 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@ -16,11 +17,13 @@ using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.Countdown;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Tests.Resources;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
@ -68,6 +71,139 @@ namespace osu.Game.Tests.Visual.Multiplayer
};
});
[Test]
public void TestStartWithCountdown()
{
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerReadyButton.CountdownButton>().SingleOrDefault()?.IsPresent == true);
ClickButtonWhenEnabled<MultiplayerReadyButton.CountdownButton>();
AddStep("click the first countdown button", () =>
{
var popoverButton = this.ChildrenOfType<MultiplayerReadyButton.CountdownButton.PopoverButton>().First();
InputManager.MoveMouseTo(popoverButton);
InputManager.Click(MouseButton.Left);
});
AddAssert("countdown button not visible", () => !this.ChildrenOfType<MultiplayerReadyButton.CountdownButton>().Single().IsPresent);
AddStep("finish countdown", () => MultiplayerClient.FinishCountDown());
AddUntilStep("match started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.WaitingForLoad);
}
[Test]
public void TestCancelCountdown()
{
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerReadyButton.CountdownButton>().SingleOrDefault()?.IsPresent == true);
ClickButtonWhenEnabled<MultiplayerReadyButton.CountdownButton>();
AddStep("click the first countdown button", () =>
{
var popoverButton = this.ChildrenOfType<MultiplayerReadyButton.CountdownButton.PopoverButton>().First();
InputManager.MoveMouseTo(popoverButton);
InputManager.Click(MouseButton.Left);
});
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
AddStep("finish countdown", () => MultiplayerClient.FinishCountDown());
AddUntilStep("match not started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
}
[Test]
public void TestReadyAndUnReadyDuringCountdown()
{
AddStep("add second user as host", () =>
{
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
MultiplayerClient.TransferHost(2);
});
AddStep("start with countdown", () => MultiplayerClient.SendMatchRequest(new MatchStartCountdownRequest { Delay = TimeSpan.FromMinutes(2) }));
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
}
[Test]
public void TestCountdownButtonEnablementAndVisibilityWhileSpectating()
{
AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
AddAssert("countdown button is visible", () => this.ChildrenOfType<MultiplayerReadyButton.CountdownButton>().Single().IsPresent);
AddAssert("countdown button disabled", () => !this.ChildrenOfType<MultiplayerReadyButton.CountdownButton>().Single().Enabled.Value);
AddStep("add second user", () => MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }));
AddAssert("countdown button disabled", () => !this.ChildrenOfType<MultiplayerReadyButton.CountdownButton>().Single().Enabled.Value);
AddStep("set second user ready", () => MultiplayerClient.ChangeUserState(2, MultiplayerUserState.Ready));
AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerReadyButton.CountdownButton>().Single().Enabled.Value);
}
[Test]
public void TestSpectatingDuringCountdownWithNoReadyUsersCancelsCountdown()
{
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerReadyButton.CountdownButton>().SingleOrDefault()?.IsPresent == true);
ClickButtonWhenEnabled<MultiplayerReadyButton.CountdownButton>();
AddStep("click the first countdown button", () =>
{
var popoverButton = this.ChildrenOfType<MultiplayerReadyButton.CountdownButton.PopoverButton>().First();
InputManager.MoveMouseTo(popoverButton);
InputManager.Click(MouseButton.Left);
});
AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
AddStep("finish countdown", () => MultiplayerClient.FinishCountDown());
AddUntilStep("match not started", () => MultiplayerClient.Room?.State == MultiplayerRoomState.Open);
}
[Test]
public void TestReadyButtonEnabledWhileSpectatingDuringCountdown()
{
AddStep("add second user", () => MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }));
AddStep("set second user ready", () => MultiplayerClient.ChangeUserState(2, MultiplayerUserState.Ready));
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerReadyButton.CountdownButton>().SingleOrDefault()?.IsPresent == true);
ClickButtonWhenEnabled<MultiplayerReadyButton.CountdownButton>();
AddStep("click the first countdown button", () =>
{
var popoverButton = this.ChildrenOfType<MultiplayerReadyButton.CountdownButton.PopoverButton>().First();
InputManager.MoveMouseTo(popoverButton);
InputManager.Click(MouseButton.Left);
});
AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
AddAssert("ready button enabled", () => this.ChildrenOfType<MultiplayerReadyButton.ReadyButton>().Single().Enabled.Value);
}
[Test]
public void TestBecomeHostDuringCountdownAndReady()
{
AddStep("add second user as host", () =>
{
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
MultiplayerClient.TransferHost(2);
});
AddStep("start countdown", () => MultiplayerClient.SendMatchRequest(new MatchStartCountdownRequest { Delay = TimeSpan.FromMinutes(1) }));
AddUntilStep("countdown started", () => MultiplayerClient.Room?.Countdown != null);
AddStep("transfer host to local user", () => MultiplayerClient.TransferHost(API.LocalUser.Value.OnlineID));
AddUntilStep("local user is host", () => MultiplayerClient.Room?.Host?.Equals(MultiplayerClient.LocalUser) == true);
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
AddAssert("countdown still active", () => MultiplayerClient.Room?.Countdown != null);
}
[Test]
public void TestDeletedBeatmapDisableReady()
{

View File

@ -16,6 +16,7 @@ using osu.Framework.Logging;
using osu.Game.Database;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer.Countdown;
using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Rulesets;
@ -534,7 +535,24 @@ namespace osu.Game.Online.Multiplayer
public Task MatchEvent(MatchServerEvent e)
{
// not used by any match types just yet.
if (Room == null)
return Task.CompletedTask;
Scheduler.Add(() =>
{
if (Room == null)
return;
switch (e)
{
case CountdownChangedEvent countdownChangedEvent:
Room.Countdown = countdownChangedEvent.Countdown;
break;
}
RoomUpdated?.Invoke();
}, false);
return Task.CompletedTask;
}

View File

@ -14,20 +14,18 @@ namespace osu.Game.Screens.OnlinePlay.Components
public abstract class ReadyButton : TriangleButton, IHasTooltip
{
public new readonly BindableBool Enabled = new BindableBool();
private IBindable<BeatmapAvailability> availability;
protected readonly IBindable<BeatmapAvailability> Availability = new Bindable<BeatmapAvailability>();
[BackgroundDependencyLoader]
private void load(OnlinePlayBeatmapAvailabilityTracker beatmapTracker)
{
availability = beatmapTracker.Availability.GetBoundCopy();
availability.BindValueChanged(_ => updateState());
Availability.BindTo(beatmapTracker.Availability);
Availability.BindValueChanged(_ => updateState());
Enabled.BindValueChanged(_ => updateState(), true);
}
private void updateState() =>
base.Enabled.Value = availability.Value.State == DownloadState.LocallyAvailable && Enabled.Value;
base.Enabled.Value = Availability.Value.State == DownloadState.LocallyAvailable && Enabled.Value;
public virtual LocalisableString TooltipText
{
@ -36,7 +34,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
if (Enabled.Value)
return string.Empty;
if (availability.Value.State != DownloadState.LocallyAvailable)
if (Availability.Value.State != DownloadState.LocallyAvailable)
return "Beatmap not downloaded";
return string.Empty;

View File

@ -17,12 +17,14 @@ using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Framework.Threading;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.Countdown;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
@ -124,6 +126,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
return;
}
// Local user is the room host and is in a ready state.
// The only action they can take is to stop a countdown if one's currently running.
if (Room.Countdown != null)
{
stopCountdown();
return;
}
// And if a countdown isn't running, start the match.
startMatch();
@ -131,6 +141,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
void toggleReady() => Client.ToggleReady().ContinueWith(_ => endOperation());
void stopCountdown() => Client.SendMatchRequest(new StopCountdownRequest()).ContinueWith(_ => endOperation());
void startMatch() => Client.StartMatch().ContinueWith(t =>
{
// accessing Exception here silences any potential errors from the antecedent task
@ -146,6 +158,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private void startCountdown(TimeSpan duration)
{
Debug.Assert(clickOperation == null);
clickOperation = ongoingOperationTracker.BeginOperation();
Client.SendMatchRequest(new MatchStartCountdownRequest { Delay = duration }).ContinueWith(_ => endOperation());
}
private void endOperation()
@ -167,16 +183,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
int newCountReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready);
int newCountTotal = Room.Users.Count(u => u.State != MultiplayerUserState.Spectating);
switch (localUser?.State)
if (Room.Countdown != null)
countdownButton.Alpha = 0;
else
{
default:
countdownButton.Alpha = 0;
break;
switch (localUser?.State)
{
default:
countdownButton.Alpha = 0;
break;
case MultiplayerUserState.Spectating:
case MultiplayerUserState.Ready:
countdownButton.Alpha = Room.Host?.Equals(localUser) == true ? 1 : 0;
break;
case MultiplayerUserState.Spectating:
case MultiplayerUserState.Ready:
countdownButton.Alpha = Room.Host?.Equals(localUser) == true ? 1 : 0;
break;
}
}
enabled.Value =
@ -232,6 +253,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
onRoomUpdated();
}
protected override void Update()
{
base.Update();
if (room?.Countdown != null)
{
// Update the countdown timer.
onRoomUpdated();
}
}
private void onRoomUpdated()
{
updateButtonText();
@ -251,21 +283,39 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
int countReady = room.Users.Count(u => u.State == MultiplayerUserState.Ready);
int countTotal = room.Users.Count(u => u.State != MultiplayerUserState.Spectating);
string countdownText = room.Countdown == null ? string.Empty : $"Starting in {room.Countdown.EndTime - DateTimeOffset.Now:mm\\:ss}";
string countText = $"({countReady} / {countTotal} ready)";
switch (localUser?.State)
if (room.Countdown != null)
{
default:
Text = "Ready";
break;
switch (localUser?.State)
{
default:
Text = $"Ready ({countdownText.ToLowerInvariant()})";
break;
case MultiplayerUserState.Spectating:
case MultiplayerUserState.Ready:
Text = room.Host?.Equals(localUser) == true
? $"Start match {countText}"
: $"Waiting for host... {countText}";
case MultiplayerUserState.Spectating:
case MultiplayerUserState.Ready:
Text = $"{countdownText} {countText}";
break;
}
}
else
{
switch (localUser?.State)
{
default:
Text = "Ready";
break;
break;
case MultiplayerUserState.Spectating:
case MultiplayerUserState.Ready:
Text = room.Host?.Equals(localUser) == true
? $"Start match {countText}"
: $"Waiting for host... {countText}";
break;
}
}
}
@ -279,20 +329,37 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
var localUser = multiplayerClient.LocalUser;
switch (localUser?.State)
if (room.Countdown != null)
{
default:
setGreen();
break;
case MultiplayerUserState.Spectating:
case MultiplayerUserState.Ready:
if (room?.Host?.Equals(localUser) == true)
switch (localUser?.State)
{
default:
setGreen();
else
setYellow();
break;
break;
case MultiplayerUserState.Spectating:
case MultiplayerUserState.Ready:
setYellow();
break;
}
}
else
{
switch (localUser?.State)
{
default:
setGreen();
break;
case MultiplayerUserState.Spectating:
case MultiplayerUserState.Ready:
if (room?.Host?.Equals(localUser) == true)
setGreen();
else
setYellow();
break;
}
}
void setYellow()
@ -317,6 +384,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
if (multiplayerClient != null)
multiplayerClient.RoomUpdated -= onRoomUpdated;
}
public override LocalisableString TooltipText
{
get
{
if (room?.Countdown != null && multiplayerClient.IsHost && multiplayerClient.LocalUser?.State == MultiplayerUserState.Ready)
return "Cancel countdown";
return base.TooltipText;
}
}
}
public class CountdownButton : IconButton, IHasPopover

View File

@ -7,12 +7,14 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.Countdown;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Mods;
@ -114,12 +116,24 @@ namespace osu.Game.Tests.Visual.Multiplayer
public void ChangeUserState(int userId, MultiplayerUserState newState)
{
Debug.Assert(Room != null);
((IMultiplayerClient)this).UserStateChanged(userId, newState);
Schedule(() =>
{
switch (Room.State)
{
case MultiplayerRoomState.Open:
// If there are no remaining ready users or the host is not ready, stop any existing countdown.
// Todo: When we have an "automatic start" mode, this should also start a new countdown if any users _are_ ready.
// Todo: This doesn't yet support non-match-start countdowns.
bool shouldStopCountdown = Room.Users.All(u => u.State != MultiplayerUserState.Ready);
shouldStopCountdown |= Room.Host?.State != MultiplayerUserState.Ready && Room.Host?.State != MultiplayerUserState.Spectating;
if (shouldStopCountdown)
countdownStopSource?.Cancel();
break;
case MultiplayerRoomState.WaitingForLoad:
if (Room.Users.All(u => u.State != MultiplayerUserState.WaitingForLoad))
{
@ -282,6 +296,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
return Task.CompletedTask;
}
private CancellationTokenSource? countdownFinishSource;
private CancellationTokenSource? countdownStopSource;
private Task countdownTask = Task.CompletedTask;
public void FinishCountDown() => countdownFinishSource?.Cancel();
public override async Task SendMatchRequest(MatchUserRequest request)
{
Debug.Assert(Room != null);
@ -289,6 +309,71 @@ namespace osu.Game.Tests.Visual.Multiplayer
switch (request)
{
case MatchStartCountdownRequest matchCountdownRequest:
countdownStopSource?.Cancel();
var stopSource = countdownStopSource = new CancellationTokenSource();
var finishSource = countdownFinishSource = new CancellationTokenSource();
var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(stopSource.Token, finishSource.Token);
var countdown = new MatchStartCountdown { EndTime = DateTimeOffset.Now + matchCountdownRequest.Delay };
Task lastCountdownTask = countdownTask;
countdownTask = start();
async Task start()
{
try
{
await lastCountdownTask;
}
catch (OperationCanceledException)
{
}
Schedule(() =>
{
if (stopSource.IsCancellationRequested)
return;
Room.Countdown = countdown;
MatchEvent(new CountdownChangedEvent { Countdown = countdown });
});
try
{
await Task.Delay(matchCountdownRequest.Delay, cancellationSource.Token).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
}
Schedule(() =>
{
if (Room.Countdown != countdown)
return;
Room.Countdown = null;
MatchEvent(new CountdownChangedEvent { Countdown = null });
using (cancellationSource)
{
if (stopSource.Token.IsCancellationRequested)
return;
}
StartMatch().WaitSafely();
});
}
break;
case StopCountdownRequest _:
countdownStopSource?.Cancel();
Room.Countdown = null;
await MatchEvent(new CountdownChangedEvent { Countdown = Room.Countdown });
break;
case ChangeTeamRequest changeTeam:
TeamVersusRoomState roomState = (TeamVersusRoomState)Room.MatchState!;
@ -307,7 +392,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
}
public override Task StartMatch()
public override async Task StartMatch()
{
Debug.Assert(Room != null);
@ -315,7 +400,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready))
ChangeUserState(user.UserID, MultiplayerUserState.WaitingForLoad);
return ((IMultiplayerClient)this).LoadRequested();
await ((IMultiplayerClient)this).LoadRequested();
}
public override Task AbortGameplay()