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. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -16,11 +17,13 @@ using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.Countdown;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osuTK; using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer 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] [Test]
public void TestDeletedBeatmapDisableReady() public void TestDeletedBeatmapDisableReady()
{ {

View File

@ -16,6 +16,7 @@ using osu.Framework.Logging;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer.Countdown;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -534,7 +535,24 @@ namespace osu.Game.Online.Multiplayer
public Task MatchEvent(MatchServerEvent e) 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; return Task.CompletedTask;
} }

View File

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

View File

@ -17,12 +17,14 @@ using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.Countdown;
using osuTK; using osuTK;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
@ -124,6 +126,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
return; 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. // And if a countdown isn't running, start the match.
startMatch(); startMatch();
@ -131,6 +141,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
void toggleReady() => Client.ToggleReady().ContinueWith(_ => endOperation()); void toggleReady() => Client.ToggleReady().ContinueWith(_ => endOperation());
void stopCountdown() => Client.SendMatchRequest(new StopCountdownRequest()).ContinueWith(_ => endOperation());
void startMatch() => Client.StartMatch().ContinueWith(t => void startMatch() => Client.StartMatch().ContinueWith(t =>
{ {
// accessing Exception here silences any potential errors from the antecedent task // 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) private void startCountdown(TimeSpan duration)
{ {
Debug.Assert(clickOperation == null);
clickOperation = ongoingOperationTracker.BeginOperation();
Client.SendMatchRequest(new MatchStartCountdownRequest { Delay = duration }).ContinueWith(_ => endOperation());
} }
private void 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 newCountReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready);
int newCountTotal = Room.Users.Count(u => u.State != MultiplayerUserState.Spectating); int newCountTotal = Room.Users.Count(u => u.State != MultiplayerUserState.Spectating);
switch (localUser?.State) if (Room.Countdown != null)
countdownButton.Alpha = 0;
else
{ {
default: switch (localUser?.State)
countdownButton.Alpha = 0; {
break; default:
countdownButton.Alpha = 0;
break;
case MultiplayerUserState.Spectating: case MultiplayerUserState.Spectating:
case MultiplayerUserState.Ready: case MultiplayerUserState.Ready:
countdownButton.Alpha = Room.Host?.Equals(localUser) == true ? 1 : 0; countdownButton.Alpha = Room.Host?.Equals(localUser) == true ? 1 : 0;
break; break;
}
} }
enabled.Value = enabled.Value =
@ -232,6 +253,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
onRoomUpdated(); onRoomUpdated();
} }
protected override void Update()
{
base.Update();
if (room?.Countdown != null)
{
// Update the countdown timer.
onRoomUpdated();
}
}
private void onRoomUpdated() private void onRoomUpdated()
{ {
updateButtonText(); updateButtonText();
@ -251,21 +283,39 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
int countReady = room.Users.Count(u => u.State == MultiplayerUserState.Ready); int countReady = room.Users.Count(u => u.State == MultiplayerUserState.Ready);
int countTotal = room.Users.Count(u => u.State != MultiplayerUserState.Spectating); 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)"; string countText = $"({countReady} / {countTotal} ready)";
switch (localUser?.State) if (room.Countdown != null)
{ {
default: switch (localUser?.State)
Text = "Ready"; {
break; default:
Text = $"Ready ({countdownText.ToLowerInvariant()})";
break;
case MultiplayerUserState.Spectating: case MultiplayerUserState.Spectating:
case MultiplayerUserState.Ready: case MultiplayerUserState.Ready:
Text = room.Host?.Equals(localUser) == true Text = $"{countdownText} {countText}";
? $"Start match {countText}" break;
: $"Waiting for host... {countText}"; }
}
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; var localUser = multiplayerClient.LocalUser;
switch (localUser?.State) if (room.Countdown != null)
{ {
default: switch (localUser?.State)
setGreen(); {
break; default:
case MultiplayerUserState.Spectating:
case MultiplayerUserState.Ready:
if (room?.Host?.Equals(localUser) == true)
setGreen(); setGreen();
else break;
setYellow();
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() void setYellow()
@ -317,6 +384,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
if (multiplayerClient != null) if (multiplayerClient != null)
multiplayerClient.RoomUpdated -= onRoomUpdated; 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 public class CountdownButton : IconButton, IHasPopover

View File

@ -7,12 +7,14 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.Countdown;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -114,12 +116,24 @@ namespace osu.Game.Tests.Visual.Multiplayer
public void ChangeUserState(int userId, MultiplayerUserState newState) public void ChangeUserState(int userId, MultiplayerUserState newState)
{ {
Debug.Assert(Room != null); Debug.Assert(Room != null);
((IMultiplayerClient)this).UserStateChanged(userId, newState); ((IMultiplayerClient)this).UserStateChanged(userId, newState);
Schedule(() => Schedule(() =>
{ {
switch (Room.State) 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: case MultiplayerRoomState.WaitingForLoad:
if (Room.Users.All(u => u.State != MultiplayerUserState.WaitingForLoad)) if (Room.Users.All(u => u.State != MultiplayerUserState.WaitingForLoad))
{ {
@ -282,6 +296,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
return Task.CompletedTask; 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) public override async Task SendMatchRequest(MatchUserRequest request)
{ {
Debug.Assert(Room != null); Debug.Assert(Room != null);
@ -289,6 +309,71 @@ namespace osu.Game.Tests.Visual.Multiplayer
switch (request) 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: case ChangeTeamRequest changeTeam:
TeamVersusRoomState roomState = (TeamVersusRoomState)Room.MatchState!; 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); 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)) foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready))
ChangeUserState(user.UserID, MultiplayerUserState.WaitingForLoad); ChangeUserState(user.UserID, MultiplayerUserState.WaitingForLoad);
return ((IMultiplayerClient)this).LoadRequested(); await ((IMultiplayerClient)this).LoadRequested();
} }
public override Task AbortGameplay() public override Task AbortGameplay()