diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs index 2f1b768ea656..4e2c3f81dac6 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs @@ -50,47 +50,6 @@ private void load() Dependencies.CacheAs(ongoingOperationTracker = new OngoingOperationTracker()); Dependencies.CacheAs(availabilityTracker.Object); - availabilityTracker.SetupGet(a => a.Availability).Returns(beatmapAvailability); - - multiplayerClient.SetupGet(m => m.LocalUser).Returns(() => localUser); - multiplayerClient.SetupGet(m => m.Room).Returns(() => multiplayerRoom); - - // By default, the local user is to be the host. - multiplayerClient.SetupGet(m => m.IsHost).Returns(() => ReferenceEquals(multiplayerRoom.Host, localUser)); - - // Assume all state changes are accepted by the server. - multiplayerClient.Setup(m => m.ChangeState(It.IsAny())) - .Callback((MultiplayerUserState r) => - { - Logger.Log($"Changing local user state from {localUser.State} to {r}"); - localUser.State = r; - raiseRoomUpdated(); - }); - - multiplayerClient.Setup(m => m.StartMatch()) - .Callback(() => - { - multiplayerClient.Raise(m => m.LoadRequested -= null); - - // immediately "end" gameplay, as we don't care about that part of the process. - changeUserState(localUser.UserID, MultiplayerUserState.Idle); - }); - - multiplayerClient.Setup(m => m.SendMatchRequest(It.IsAny())) - .Callback((MatchUserRequest request) => - { - switch (request) - { - case StartMatchCountdownRequest countdownStart: - setRoomCountdown(countdownStart.Duration); - break; - - case StopCountdownRequest: - clearRoomCountdown(); - break; - } - }); - Children = new Drawable[] { ongoingOperationTracker, @@ -103,10 +62,51 @@ public void SetUpSteps() { AddStep("reset state", () => { - multiplayerClient.Invocations.Clear(); + multiplayerClient.Reset(); + multiplayerClient.SetupGet(m => m.LocalUser).Returns(() => localUser); + multiplayerClient.SetupGet(m => m.Room).Returns(() => multiplayerRoom); + + // By default, the local user is to be the host. + multiplayerClient.SetupGet(m => m.IsHost).Returns(() => ReferenceEquals(multiplayerRoom.Host, localUser)); + + // Assume all state changes are accepted by the server. + multiplayerClient.Setup(m => m.ChangeState(It.IsAny())) + .Callback((MultiplayerUserState r) => + { + Logger.Log($"Changing local user state from {localUser.State} to {r}"); + localUser.State = r; + raiseRoomUpdated(); + }); + + multiplayerClient.Setup(m => m.StartMatch()) + .Callback(() => + { + multiplayerClient.Raise(m => m.LoadRequested -= null); + + // immediately "end" gameplay, as we don't care about that part of the process. + changeUserState(localUser.UserID, MultiplayerUserState.Idle); + }); + + multiplayerClient.Setup(m => m.SendMatchRequest(It.IsAny())) + .Callback((MatchUserRequest request) => + { + switch (request) + { + case StartMatchCountdownRequest countdownStart: + setRoomCountdown(countdownStart.Duration); + break; + + case StopCountdownRequest: + clearRoomCountdown(); + break; + } + }); beatmapAvailability.Value = BeatmapAvailability.LocallyAvailable(); + availabilityTracker.Reset(); + availabilityTracker.SetupGet(a => a.Availability).Returns(beatmapAvailability); + PlaylistItem item = new PlaylistItem(Beatmap.Value.BeatmapInfo) { RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID @@ -375,6 +375,22 @@ public void TestManyUsersChangingState(bool isHost) [Test] public void TestAbortMatch() + { + setUpMatchCallbacks(); + + // Ready + ClickButtonWhenEnabled(); + + // Start match + ClickButtonWhenEnabled(); + AddUntilStep("countdown button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); + + // Abort + ClickButtonWhenEnabled(); + AddStep("check abort request received", () => multiplayerClient.Verify(m => m.AbortMatch(), Times.Once)); + } + + private void setUpMatchCallbacks() { AddStep("setup client", () => { @@ -383,6 +399,7 @@ public void TestAbortMatch() { multiplayerClient.Raise(m => m.LoadRequested -= null); multiplayerClient.Object.Room!.State = MultiplayerRoomState.WaitingForLoad; + raiseRoomUpdated(); // 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); @@ -395,19 +412,133 @@ public void TestAbortMatch() raiseRoomUpdated(); }); }); + } - // Ready + [Test] + public void TestRefereeSpectating() + { + AddStep("set up referee", () => + { + multiplayerClient.SetupGet(m => m.IsReferee).Returns(true); + multiplayerClient.SetupGet(m => m.IsHost).Returns(false); + multiplayerClient.Object.Room!.Users.Single().Role = MultiplayerRoomUserRole.Referee; + raiseRoomUpdated(); + }); + + const int users = 10; + + AddStep("add many users", () => + { + for (int i = 0; i < users; i++) + addUser(new APIUser { Id = i, Username = "Another user" }); + }); + AddAssert("button disabled", () => this.ChildrenOfType().Single().Enabled.Value, () => Is.False); + + AddStep("move to spectate", () => changeUserState(multiplayerClient.Object.LocalUser!.UserID, MultiplayerUserState.Spectating)); + + AddStep("ready up a user", () => changeUserState(9, MultiplayerUserState.Ready)); + AddAssert("button enabled", () => this.ChildrenOfType().Single().Enabled.Value, () => Is.True); + + setUpMatchCallbacks(); + + // start match ClickButtonWhenEnabled(); + AddUntilStep("countdown button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); - // Start match + // abort + ClickButtonWhenEnabled(); + AddStep("check abort request received", () => multiplayerClient.Verify(m => m.AbortMatch(), Times.Once)); + } + + [Test] + public void TestRefereeFlowWithoutCountdown() + { + AddStep("set up referee", () => + { + multiplayerClient.SetupGet(m => m.IsReferee).Returns(true); + multiplayerClient.SetupGet(m => m.IsHost).Returns(false); + multiplayerClient.Object.Room!.Users.Single().Role = MultiplayerRoomUserRole.Referee; + raiseRoomUpdated(); + }); + + const int users = 10; + + AddStep("add many users", () => + { + for (int i = 0; i < users; i++) + addUser(new APIUser { Id = i, Username = "Another user" }); + }); + AddAssert("button disabled", () => this.ChildrenOfType().Single().Enabled.Value, () => Is.False); + + AddStep("ready up a user", () => changeUserState(9, MultiplayerUserState.Ready)); + AddAssert("button enabled", () => this.ChildrenOfType().Single().Enabled.Value, () => Is.True); + + setUpMatchCallbacks(); + + // start match ClickButtonWhenEnabled(); AddUntilStep("countdown button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); - // Abort + // abort ClickButtonWhenEnabled(); AddStep("check abort request received", () => multiplayerClient.Verify(m => m.AbortMatch(), Times.Once)); } + [Test] + public void TestRefereeFlowWithCountdown() + { + AddStep("set up referee", () => + { + multiplayerClient.SetupGet(m => m.IsReferee).Returns(true); + multiplayerClient.SetupGet(m => m.IsHost).Returns(false); + multiplayerClient.Object.Room!.Users.Single().Role = MultiplayerRoomUserRole.Referee; + raiseRoomUpdated(); + }); + + const int users = 10; + + AddStep("add many users", () => + { + for (int i = 0; i < users; i++) + addUser(new APIUser { Id = i, Username = "Another user" }); + }); + AddAssert("button disabled", () => this.ChildrenOfType().Single().Enabled.Value, () => Is.False); + + AddStep("ready up a user", () => changeUserState(9, MultiplayerUserState.Ready)); + AddAssert("button enabled", () => this.ChildrenOfType().Single().Enabled.Value, () => Is.True); + + setUpMatchCallbacks(); + + AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + ClickButtonWhenEnabled(); + AddStep("click the first countdown button", () => + { + var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); + InputManager.MoveMouseTo(popoverButton); + InputManager.Click(MouseButton.Left); + }); + + AddStep("check request received", () => + { + multiplayerClient.Verify(m => m.SendMatchRequest(It.Is(req => + req.Duration == TimeSpan.FromSeconds(10) + )), Times.Once); + }); + + ClickButtonWhenEnabled(); + AddStep("click the cancel button", () => + { + var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().Last(); + InputManager.MoveMouseTo(popoverButton); + InputManager.Click(MouseButton.Left); + }); + + AddStep("check request received", () => + { + multiplayerClient.Verify(m => m.SendMatchRequest(It.IsAny()), Times.Once); + }); + } + private void verifyGameplayStartFlow() { checkLocalUserState(MultiplayerUserState.Ready); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 906cb3436c11..379a589ca964 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -353,6 +353,30 @@ public void TestChangeSettingsButtonVisibleForHost() AddAssert("button hidden", () => this.ChildrenOfType().Single().ChangeSettingsButton.Alpha, () => Is.EqualTo(0)); } + [Test] + public void TestChangeSettingsButtonAlwaysVisibleForReferee() + { + AddStep("add playlist item", () => + { + room.Playlist = + [ + new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID + } + ]; + }); + AddStep("setup referee", () => MultiplayerClient.RoomSetupAction = r => r.Host!.Role = MultiplayerRoomUserRole.Referee); + ClickButtonWhenEnabled(); + + AddUntilStep("wait for join", () => RoomJoined); + + AddUntilStep("button visible", () => this.ChildrenOfType().Single().ChangeSettingsButton.Alpha, () => Is.GreaterThan(0)); + AddStep("join other user", void () => MultiplayerClient.AddUser(new APIUser { Id = PLAYER_1_ID })); + AddStep("make other user host", () => MultiplayerClient.TransferHost(PLAYER_1_ID)); + AddAssert("button hidden", () => this.ChildrenOfType().Single().ChangeSettingsButton.Alpha, () => Is.GreaterThan(0)); + } + [Test] public void TestUserModSelectUpdatesWhenNotVisible() { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index 7f1eb50ac2bd..8aaf85923c90 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -32,10 +32,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { public partial class TestSceneMultiplayerParticipantsList : MultiplayerTestScene { - public override void SetUpSteps() + private void setUpList() { - base.SetUpSteps(); - AddStep("join room", () => JoinRoom(CreateDefaultRoom())); WaitForJoined(); createNewParticipantsList(); @@ -44,6 +42,7 @@ public override void SetUpSteps() [Test] public void TestAddUser() { + setUpList(); AddAssert("one unique panel", () => this.ChildrenOfType().Select(p => p.Current.Value).Distinct().Count() == 1); AddStep("add user", () => MultiplayerClient.AddUser(new APIUser @@ -59,6 +58,7 @@ public void TestAddUser() [Test] public void TestAddReferee() { + setUpList(); AddAssert("one unique panel", () => this.ChildrenOfType().Select(p => p.Current.Value).Distinct().Count() == 1); AddStep("add user", () => MultiplayerClient.AddUser(new MultiplayerRoomUser(3) @@ -78,6 +78,7 @@ public void TestAddReferee() [Test] public void TestAddUnresolvedUser() { + setUpList(); AddAssert("one unique panel", () => this.ChildrenOfType().Select(p => p.Current.Value).Distinct().Count() == 1); AddStep("add non-resolvable user", () => MultiplayerClient.TestAddUnresolvedUser()); @@ -94,6 +95,8 @@ public void TestAddUnresolvedUser() [Test] public void TestRemoveUser() { + setUpList(); + APIUser? secondUser = null; AddStep("add a user", () => @@ -114,6 +117,7 @@ public void TestRemoveUser() [Test] public void TestGameStateHasPriorityOverDownloadState() { + setUpList(); AddStep("set to downloading map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0))); checkProgressBarVisibility(true); @@ -128,6 +132,7 @@ public void TestGameStateHasPriorityOverDownloadState() [Test] public void TestCorrectInitialState() { + setUpList(); AddStep("set to downloading map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0))); createNewParticipantsList(); checkProgressBarVisibility(true); @@ -136,6 +141,7 @@ public void TestCorrectInitialState() [Test] public void TestBeatmapDownloadingStates() { + setUpList(); AddStep("set to unknown", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Unknown())); AddStep("set to no map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded())); AddStep("set to downloading map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0))); @@ -159,6 +165,7 @@ public void TestBeatmapDownloadingStates() [Test] public void TestToggleReadyState() { + setUpList(); AddAssert("ready mark invisible", () => !this.ChildrenOfType().Single().IsPresent); AddStep("make user ready", () => MultiplayerClient.ChangeState(MultiplayerUserState.Ready)); @@ -171,6 +178,7 @@ public void TestToggleReadyState() [Test] public void TestToggleSpectateState() { + setUpList(); AddStep("make user spectating", () => MultiplayerClient.ChangeState(MultiplayerUserState.Spectating)); AddStep("make user idle", () => MultiplayerClient.ChangeState(MultiplayerUserState.Idle)); } @@ -178,6 +186,7 @@ public void TestToggleSpectateState() [Test] public void TestCrownChangesStateWhenHostTransferred() { + setUpList(); AddStep("add user", () => MultiplayerClient.AddUser(new APIUser { Id = 3, @@ -201,6 +210,7 @@ public void TestCrownChangesStateWhenHostTransferred() [Test] public void TestHostGetsPinnedToTop() { + setUpList(); AddStep("add user", () => MultiplayerClient.AddUser(new APIUser { Id = 3, @@ -218,8 +228,9 @@ public void TestHostGetsPinnedToTop() } [Test] - public void TestKickButtonOnlyPresentWhenHost() + public void TestKickButtonPresentWhenHost() { + setUpList(); AddStep("add user", () => MultiplayerClient.AddUser(new APIUser { Id = 3, @@ -238,9 +249,33 @@ public void TestKickButtonOnlyPresentWhenHost() AddUntilStep("kick buttons visible", () => this.ChildrenOfType().Count(d => d.IsPresent) == 1); } + [Test] + public void TestKickButtonPresentWhenReferee() + { + AddStep("set up referee", () => MultiplayerClient.RoomSetupAction = r => r.Host!.Role = MultiplayerRoomUserRole.Referee); + setUpList(); + AddStep("add user", () => MultiplayerClient.AddUser(new APIUser + { + Id = 3, + Username = "Second", + CoverUrl = TestResources.COVER_IMAGE_3, + })); + + AddUntilStep("kick buttons visible", () => this.ChildrenOfType().Count(d => d.IsPresent) == 1); + + AddStep("make second user host", () => MultiplayerClient.TransferHost(3)); + + AddUntilStep("kick buttons visible", () => this.ChildrenOfType().Count(d => d.IsPresent) == 1); + + AddStep("make local user host again", () => MultiplayerClient.TransferHost(API.LocalUser.Value.Id)); + + AddUntilStep("kick buttons visible", () => this.ChildrenOfType().Count(d => d.IsPresent) == 1); + } + [Test] public void TestKickButtonKicks() { + setUpList(); AddStep("add user", () => MultiplayerClient.AddUser(new APIUser { Id = 3, @@ -258,6 +293,7 @@ public void TestManyUsers() { const int users_count = 200; + setUpList(); AddStep("add many users", () => { for (int i = 0; i < users_count; i++) @@ -316,6 +352,7 @@ public void TestManyUsers() [Test] public void TestUserWithMods() { + setUpList(); AddStep("add user", () => { MultiplayerClient.AddUser(new APIUser @@ -353,6 +390,7 @@ public void TestUserWithMods() [Test] public void TestUserWithStyle() { + setUpList(); AddStep("add users", () => { MultiplayerClient.AddUser(new APIUser @@ -380,6 +418,7 @@ public void TestUserWithStyle() [Test] public void TestModOverlap() { + setUpList(); AddStep("add dummy mods", () => { MultiplayerClient.ChangeUserMods(new Mod[] @@ -438,6 +477,7 @@ public void TestModOverlap() [Test] public void TestModsAndRuleset() { + setUpList(); AddStep("add another user", () => { MultiplayerClient.AddUser(new APIUser @@ -472,6 +512,7 @@ public void TestModsAndRuleset() [Test] public void TestTeams() { + setUpList(); AddStep("enable teams", () => MultiplayerClient.ChangeSettings(matchType: MatchType.TeamVersus)); AddAssert("one unique panel", () => this.ChildrenOfType().Select(p => p.Current.Value).Distinct().Count() == 1); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index 2f54551fa89d..68f06d9f7474 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -41,10 +41,8 @@ private void load(GameHost host, AudioManager audio) Dependencies.Cache(Realm); } - public override void SetUpSteps() + private void setUpRoom() { - base.SetUpSteps(); - AddStep("create room", () => room = CreateDefaultRoom()); AddStep("join room", () => JoinRoom(room)); WaitForJoined(); @@ -80,6 +78,8 @@ public override void SetUpSteps() [Test] public void TestDeleteButtonAlwaysVisibleForHost() { + setUpRoom(); + AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode == QueueMode.AllPlayers); @@ -92,6 +92,8 @@ public void TestDeleteButtonAlwaysVisibleForHost() [Test] public void TestDeleteButtonOnlyVisibleForItemOwnerIfNotHost() { + setUpRoom(); + AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode == QueueMode.AllPlayers); @@ -108,9 +110,35 @@ public void TestDeleteButtonOnlyVisibleForItemOwnerIfNotHost() assertDeleteButtonVisibility(2, true); } + [Test] + public void TestDeleteButtonAlwaysVisibleForReferee() + { + AddStep("ensure host will be referee", () => MultiplayerClient.RoomSetupAction = r => r.Host!.Role = MultiplayerRoomUserRole.Referee); + setUpRoom(); + + AddStep("join other user", () => MultiplayerClient.AddUser(new APIUser { Id = 1234 })); + + AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); + AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode == QueueMode.AllPlayers); + + addPlaylistItem(() => API.LocalUser.Value.OnlineID); + assertDeleteButtonVisibility(1, true); + addPlaylistItem(() => 1234); + assertDeleteButtonVisibility(2, true); + + AddStep("set host only queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.HostOnly }).WaitSafely()); + AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode == QueueMode.HostOnly); + AddStep("set other user as host", () => MultiplayerClient.TransferHost(1234)); + + assertDeleteButtonVisibility(1, true); + assertDeleteButtonVisibility(2, true); + } + [Test] public void TestSingleItemDoesNotHaveDeleteButton() { + setUpRoom(); + AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode == QueueMode.AllPlayers); @@ -120,6 +148,8 @@ public void TestSingleItemDoesNotHaveDeleteButton() [Test] public void TestCurrentItemHasDeleteButtonIfNotSingle() { + setUpRoom(); + AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode == QueueMode.AllPlayers); @@ -139,6 +169,8 @@ public void TestCurrentItemHasDeleteButtonIfNotSingle() [Test] public void TestChangeExistingItem() { + setUpRoom(); + AddStep("change beatmap", () => MultiplayerClient.EditPlaylistItem(new MultiplayerPlaylistItem { ID = playlist.Items[0].ID, diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index ff974e2e6dd5..5742548fb9f9 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -196,6 +196,11 @@ public virtual bool IsHost } } + /// + /// Whether the is a referee in the . + /// + public virtual bool IsReferee => LocalUser?.Role == MultiplayerRoomUserRole.Referee; + [Resolved] protected IAPIProvider API { get; private set; } = null!; diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs index 3c02565fa19c..c55b4001c6d7 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs @@ -92,7 +92,7 @@ public MultiplayerRoom(Room room) /// Determines whether a user is able to add playlist items to this room. /// /// The user to check. - public bool CanAddPlaylistItems(MultiplayerRoomUser user) => user.Equals(Host) || Settings.QueueMode != QueueMode.HostOnly; + public bool CanAddPlaylistItems(MultiplayerRoomUser user) => user.Equals(Host) || user.Role == MultiplayerRoomUserRole.Referee || Settings.QueueMode != QueueMode.HostOnly; public override string ToString() => $"RoomID:{RoomID} Host:{Host?.UserID} Users:{Users.Count} State:{State} Settings: [{Settings}]"; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs index 97f30035cf88..196eafa20cdd 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs @@ -32,6 +32,8 @@ public partial class MatchStartControl : CompositeDrawable [Resolved] private MultiplayerClient client { get; set; } = null!; + private MatchStartCountdown? currentMatchStartCountdown => client.Room?.ActiveCountdowns.OfType().SingleOrDefault(); + private readonly MultiplayerReadyButton readyButton; private readonly MultiplayerCountdownButton countdownButton; @@ -111,22 +113,24 @@ private void onReadyButtonClick() Debug.Assert(clickOperation == null); clickOperation = ongoingOperationTracker.BeginOperation(); - if (client.IsHost) + if (client.IsReferee) + { + if (client.Room.State == MultiplayerRoomState.Open && currentMatchStartCountdown == null) + startMatch(); + else if (client.Room.State == MultiplayerRoomState.WaitingForLoad || client.Room.State == MultiplayerRoomState.Playing) + abortMatch(); + } + else if (client.IsHost) { if (client.Room.State == MultiplayerRoomState.Open) { - if (isReady() && !client.Room.ActiveCountdowns.Any(c => c is MatchStartCountdown)) + if (isReady() && currentMatchStartCountdown == null) startMatch(); else toggleReady(); } else - { - if (dialogOverlay == null) - abortMatch(); - else - dialogOverlay.Push(new ConfirmAbortDialog(abortMatch, endOperation)); - } + abortMatch(); } else if (client.Room.State != MultiplayerRoomState.Closed) toggleReady(); @@ -146,7 +150,15 @@ void startMatch() => client.StartMatch().FireAndForget(onSuccess: () => endOperation(); }); - void abortMatch() => client.AbortMatch().FireAndForget(endOperation, _ => endOperation()); + void performAbort() => client.AbortMatch().FireAndForget(endOperation, _ => endOperation()); + + void abortMatch() + { + if (dialogOverlay == null) + performAbort(); + else + dialogOverlay.Push(new ConfirmAbortDialog(performAbort, endOperation)); + } } private void startCountdown(TimeSpan duration) @@ -159,14 +171,13 @@ private void startCountdown(TimeSpan duration) private void cancelCountdown() { - if (client.Room == null) + if (client.Room == null || currentMatchStartCountdown == null) return; Debug.Assert(clickOperation == null); clickOperation = ongoingOperationTracker.BeginOperation(); - MultiplayerCountdown countdown = client.Room.ActiveCountdowns.Single(c => c is MatchStartCountdown); - client.SendMatchRequest(new StopCountdownRequest(countdown.ID)).ContinueWith(_ => endOperation()); + client.SendMatchRequest(new StopCountdownRequest(currentMatchStartCountdown.ID)).ContinueWith(_ => endOperation()); } private void endOperation() @@ -186,10 +197,10 @@ private void updateState() var localUser = client.LocalUser; - int newCountReady = client.Room.Users.Count(u => u.State == MultiplayerUserState.Ready); - int newCountTotal = client.Room.Users.Count(u => u.State != MultiplayerUserState.Spectating); + int newCountReady = client.Room.Users.Count(u => u.Role == MultiplayerRoomUserRole.Player && u.State == MultiplayerUserState.Ready); + int newCountTotal = client.Room.Users.Count(u => u.Role == MultiplayerRoomUserRole.Player && u.State != MultiplayerUserState.Spectating); - if (!client.IsHost || client.Room.Settings.AutoStartEnabled) + if ((!client.IsHost && !client.IsReferee) || client.Room.Settings.AutoStartEnabled || client.Room.State != MultiplayerRoomState.Open) countdownButton.Hide(); else { @@ -214,12 +225,16 @@ private void updateState() // When the local user is the host and spectating the match, the ready button should be enabled only if any users are ready. if (localUser?.State == MultiplayerUserState.Spectating) - readyButton.Enabled.Value &= client.IsHost && newCountReady > 0 && !client.Room.ActiveCountdowns.Any(c => c is MatchStartCountdown); + readyButton.Enabled.Value &= (client.IsHost || client.IsReferee) && newCountReady > 0 && currentMatchStartCountdown == null; - // When the local user is not the host, the button should only be enabled when no match is in progress. - if (!client.IsHost) + // When the local user is not the host or a referee, the button should only be enabled when no match is in progress. + if (!client.IsHost && !client.IsReferee) readyButton.Enabled.Value &= client.Room.State == MultiplayerRoomState.Open; + // As a referee, readying up should not be possible, so if there is no match going on and no users readied up, prevent a match start. + if (client.IsReferee) + readyButton.Enabled.Value &= client.Room.State != MultiplayerRoomState.Open || newCountReady > 0; + // At all times, the countdown button should only be enabled when no match is in progress. countdownButton.Enabled.Value &= client.Room.State == MultiplayerRoomState.Open; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs index 50e996d266b6..c885e613e4d2 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs @@ -120,7 +120,7 @@ public Popover GetPopover() }); } - if (multiplayerClient.Room?.ActiveCountdowns.Any(c => c is MatchStartCountdown) == true && multiplayerClient.IsHost) + if (multiplayerClient.Room?.ActiveCountdowns.Any(c => c is MatchStartCountdown) == true && (multiplayerClient.IsHost || multiplayerClient.IsReferee)) { flow.Add(new RoundedButton { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 876f1cbd56ba..cbad4a07e949 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -122,14 +122,24 @@ private void updateButtonText() var localUser = multiplayerClient.LocalUser; - int countReady = room.Users.Count(u => u.State == MultiplayerUserState.Ready); - int countTotal = room.Users.Count(u => u.State != MultiplayerUserState.Spectating); + int countReady = room.Users.Count(u => u.Role == MultiplayerRoomUserRole.Player && u.State == MultiplayerUserState.Ready); + int countTotal = room.Users.Count(u => u.Role == MultiplayerRoomUserRole.Player && u.State != MultiplayerUserState.Spectating); string countText = $"({countReady} / {countTotal} ready)"; - if (countdown != null) + string? countdownText = countdown != null ? $"Starting in {countdownTimeRemaining:mm\\:ss}" : null; + + if (multiplayerClient.IsReferee) { - string countdownText = $"Starting in {countdownTimeRemaining:mm\\:ss}"; + if (room.State == MultiplayerRoomState.Open) + Text = countReady == 0 ? $"Waiting for players... {countText}" : $"{countdownText ?? "Start match"} {countText}"; + else + Text = "Abort match"; + return; + } + + if (countdownText != null) + { switch (localUser?.State) { default: @@ -196,7 +206,7 @@ private void updateButtonColour() { default: // Show the abort button for the host as long as gameplay is in progress. - if (multiplayerClient.IsHost && room.State != MultiplayerRoomState.Open) + if ((multiplayerClient.IsHost || multiplayerClient.IsReferee) && room.State != MultiplayerRoomState.Open) setRed(); else setGreen(); @@ -204,7 +214,7 @@ private void updateButtonColour() case MultiplayerUserState.Spectating: case MultiplayerUserState.Ready: - if (multiplayerClient.IsHost && !room.ActiveCountdowns.Any(c => c is MatchStartCountdown)) + if ((multiplayerClient.IsHost || multiplayerClient.IsReferee) && !room.ActiveCountdowns.Any(c => c is MatchStartCountdown)) setGreen(); else setYellow(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs index dc6a7139080c..fbd0a4cb193c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs @@ -66,8 +66,8 @@ private void updateDeleteButtonVisibility() if (multiplayerClient.Room == null) return; - bool isItemOwner = Item.OwnerID == api.LocalUser.Value.OnlineID || multiplayerClient.IsHost; - bool isValidItem = isItemOwner && !Item.Expired; + bool isItemOwnerOrReferee = Item.OwnerID == api.LocalUser.Value.OnlineID || multiplayerClient.IsHost || multiplayerClient.IsReferee; + bool isValidItem = isItemOwnerOrReferee && !Item.Expired; AllowDeletion = isValidItem && (Item.ID != multiplayerClient.Room.Settings.PlaylistItemId // This is an optimisation for the following check. diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index ca8c4c7cfe40..0c31e1f0db1e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -596,7 +596,8 @@ private void onLoadRequested() break; default: - targetScreen.Push(new MultiplayerPlayerLoader(() => new MultiplayerPlayer(room, new PlaylistItem(client.Room.CurrentPlaylistItem), users))); + if (!client.IsReferee) + targetScreen.Push(new MultiplayerPlayerLoader(() => new MultiplayerPlayer(room, new PlaylistItem(client.Room.CurrentPlaylistItem), users))); break; } } @@ -676,8 +677,8 @@ private void updateGameplayState() Ruleset.Value = ruleset; Mods.Value = client.LocalUser.Mods.Concat(item.RequiredMods).Select(m => m.ToMod(rulesetInstance)).ToArray(); - bool freemods = item.Freestyle || item.AllowedMods.Any(); - bool freestyle = item.Freestyle; + bool freemods = !client.IsReferee && (item.Freestyle || item.AllowedMods.Any()); + bool freestyle = !client.IsReferee && item.Freestyle; if (freemods) userModsSection.Show(); @@ -921,8 +922,8 @@ public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset) if (client.Room.CanAddPlaylistItems(client.LocalUser) != true) return; - // If there's only one playlist item and we are the host, assume we want to change it. Else add a new one. - PlaylistItem? itemToEdit = client.IsHost && room.Playlist.Count == 1 ? room.Playlist.Single() : null; + // If there's only one playlist item and we are the host / a referee, assume we want to change it. Else add a new one. + PlaylistItem? itemToEdit = (client.IsHost || client.IsReferee) && room.Playlist.Count == 1 ? room.Playlist.Single() : null; ShowSongSelect(itemToEdit); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomPanel.cs index e52133b46b6b..9811d768463c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomPanel.cs @@ -58,7 +58,7 @@ private void onRoomUpdated() => Scheduler.AddOnce(() => if (client.Room == null || client.LocalUser == null) return; - ChangeSettingsButton.Alpha = client.IsHost ? 1 : 0; + ChangeSettingsButton.Alpha = client.IsHost || client.IsReferee ? 1 : 0; SelectedItem.Value = new PlaylistItem(client.Room.CurrentPlaylistItem); }); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index fb6001bda06c..352deb375a40 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -295,7 +295,7 @@ private void updateState() userStyleDisplay.FadeOut(fade_time); } - kickButton.Alpha = client.IsHost && !user.Equals(client.LocalUser) ? 1 : 0; + kickButton.Alpha = (client.IsHost || client.IsReferee) && !user.Equals(client.LocalUser) ? 1 : 0; crown.Alpha = client.Room.Host?.Equals(user) == true ? 1 : 0; } @@ -312,8 +312,7 @@ public MenuItem[]? ContextMenuItems if (user.UserID == api.LocalUser.Value.Id) return null; - // If the local user is not the host of the room. - if (client.Room.Host?.UserID != api.LocalUser.Value.Id) + if (!client.IsHost && !client.IsReferee) return null; int targetUser = user.UserID; @@ -322,8 +321,8 @@ public MenuItem[]? ContextMenuItems { new OsuMenuItem("Give host", MenuItemType.Standard, () => { - // Ensure the local user is still host. - if (!client.IsHost) + // Ensure the local user is still host / a referee. + if (!client.IsHost && !client.IsReferee) return; client.TransferHost(targetUser).FireAndForget(); @@ -331,7 +330,7 @@ public MenuItem[]? ContextMenuItems new OsuMenuItem("Kick", MenuItemType.Destructive, () => { // Ensure the local user is still host. - if (!client.IsHost) + if (!client.IsHost && !client.IsReferee) return; client.KickUser(targetUser).FireAndForget();