From b76a87e6f886401cdc34245e41b706f4a8d6595f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Mar 2022 18:58:09 +0900 Subject: [PATCH 01/43] Split ready button visual logic into button itself --- .../Visual/Multiplayer/QueueModeTestScene.cs | 4 +- .../TestSceneAllPlayersQueueMode.cs | 8 +- .../TestSceneMultiplayerMatchSubScreen.cs | 2 +- .../TestSceneMultiplayerReadyButton.cs | 16 +- .../Match/MultiplayerReadyButton.cs | 172 ++++++++++++------ 5 files changed, 132 insertions(+), 70 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index bafc579134..df8f63f6ed 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -95,10 +95,10 @@ namespace osu.Game.Tests.Visual.Multiplayer protected void RunGameplay() { AddUntilStep("wait for idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for player", () => multiplayerComponents.CurrentScreen is Player player && player.IsLoaded); AddStep("exit player", () => multiplayerComponents.MultiplayerScreen.MakeCurrent()); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs index 0785315b26..266ac60168 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs @@ -102,10 +102,10 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID); AddUntilStep("wait for idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for player", () => CurrentScreen is Player player && player.IsLoaded); AddAssert("ruleset is correct", () => ((Player)CurrentScreen).Ruleset.Value.Equals(new OsuRuleset().RulesetInfo)); @@ -119,10 +119,10 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID); AddUntilStep("wait for idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for player", () => CurrentScreen is Player player && player.IsLoaded); AddAssert("mods are correct", () => !((Player)CurrentScreen).Mods.Value.Any()); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 057032c413..a51d4678fe 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -145,7 +145,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for spectating user state", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("match started", () => MultiplayerClient.Room?.State == MultiplayerRoomState.WaitingForLoad); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index f34f7c6c91..7f7a4c9c5f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -92,10 +92,10 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.TransferHost(2); }); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle); } @@ -111,7 +111,7 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }); }); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); verifyGameplayStartFlow(); @@ -126,7 +126,7 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.TransferHost(2); }); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddStep("make user host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0)); verifyGameplayStartFlow(); @@ -141,12 +141,12 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }); }); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); AddStep("transfer host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[1].UserID ?? 0)); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is idle (match not started)", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle); AddAssert("ready button enabled", () => button.ChildrenOfType().Single().Enabled.Value); } @@ -166,7 +166,7 @@ namespace osu.Game.Tests.Visual.Multiplayer if (!isHost) AddStep("transfer host", () => MultiplayerClient.TransferHost(2)); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddRepeatStep("change user ready state", () => { @@ -184,7 +184,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private void verifyGameplayStartFlow() { AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user waiting for load", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad); AddStep("finish gameplay", () => diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 0c80f6ef5b..920e23eaa1 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -14,16 +14,12 @@ using osu.Framework.Threading; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Online.Multiplayer; -using osu.Game.Screens.OnlinePlay.Components; using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { public class MultiplayerReadyButton : MultiplayerRoomComposite { - [Resolved] - private OsuColour colours { get; set; } - [Resolved] private OngoingOperationTracker ongoingOperationTracker { get; set; } @@ -34,14 +30,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private Sample sampleReadyAll; private Sample sampleUnready; - private readonly ButtonWithTrianglesExposed button; + private readonly ReadyButton readyButton; private int countReady; private ScheduledDelegate readySampleDelegate; private IBindable operationInProgress; public MultiplayerReadyButton() { - InternalChild = button = new ButtonWithTrianglesExposed + InternalChild = readyButton = new ReadyButton { RelativeSizeAxes = Axes.Both, Size = Vector2.One, @@ -123,47 +119,26 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private void updateState() { - var localUser = Client.LocalUser; - - int newCountReady = Room?.Users.Count(u => u.State == MultiplayerUserState.Ready) ?? 0; - int newCountTotal = Room?.Users.Count(u => u.State != MultiplayerUserState.Spectating) ?? 0; - - switch (localUser?.State) + if (Room == null) { - default: - button.Text = "Ready"; - updateButtonColour(true); - break; - - case MultiplayerUserState.Spectating: - case MultiplayerUserState.Ready: - string countText = $"({newCountReady} / {newCountTotal} ready)"; - - if (Room?.Host?.Equals(localUser) == true) - { - button.Text = $"Start match {countText}"; - updateButtonColour(true); - } - else - { - button.Text = $"Waiting for host... {countText}"; - updateButtonColour(false); - } - - break; + readyButton.Enabled.Value = false; + return; } - bool enableButton = - Room?.State == MultiplayerRoomState.Open + var localUser = Client.LocalUser; + + int newCountReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready); + int newCountTotal = Room.Users.Count(u => u.State != MultiplayerUserState.Spectating); + + readyButton.Enabled.Value = + Room.State == MultiplayerRoomState.Open && CurrentPlaylistItem.Value?.ID == Room.Settings.PlaylistItemId && !Room.Playlist.Single(i => i.ID == Room.Settings.PlaylistItemId).Expired && !operationInProgress.Value; // When the local user is the host and spectating the match, the "start match" state should be enabled if any users are ready. if (localUser?.State == MultiplayerUserState.Spectating) - enableButton &= Room?.Host?.Equals(localUser) == true && newCountReady > 0; - - button.Enabled.Value = enableButton; + readyButton.Enabled.Value &= Room.Host?.Equals(localUser) == true && newCountReady > 0; if (newCountReady == countReady) return; @@ -187,25 +162,112 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match }); } - private void updateButtonColour(bool green) - { - if (green) - { - button.BackgroundColour = colours.Green; - button.Triangles.ColourDark = colours.Green; - button.Triangles.ColourLight = colours.GreenLight; - } - else - { - button.BackgroundColour = colours.YellowDark; - button.Triangles.ColourDark = colours.YellowDark; - button.Triangles.ColourLight = colours.Yellow; - } - } - - private class ButtonWithTrianglesExposed : ReadyButton + public class ReadyButton : Components.ReadyButton { public new Triangles Triangles => base.Triangles; + + [Resolved] + private MultiplayerClient multiplayerClient { get; set; } + + [Resolved] + private OsuColour colours { get; set; } + + [CanBeNull] + private MultiplayerRoom room => multiplayerClient.Room; + + protected override void LoadComplete() + { + base.LoadComplete(); + + multiplayerClient.RoomUpdated += () => Scheduler.AddOnce(onRoomUpdated); + onRoomUpdated(); + } + + private void onRoomUpdated() + { + updateButtonText(); + updateButtonColour(); + } + + private void updateButtonText() + { + if (room == null) + { + Text = "Ready"; + return; + } + + var localUser = multiplayerClient.LocalUser; + + int countReady = room.Users.Count(u => u.State == MultiplayerUserState.Ready); + int countTotal = room.Users.Count(u => u.State != MultiplayerUserState.Spectating); + + string countText = $"({countReady} / {countTotal} ready)"; + + switch (localUser?.State) + { + default: + Text = "Ready"; + break; + + case MultiplayerUserState.Spectating: + case MultiplayerUserState.Ready: + Text = room.Host?.Equals(localUser) == true + ? $"Start match {countText}" + : $"Waiting for host... {countText}"; + + break; + } + } + + private void updateButtonColour() + { + if (room == null) + { + setGreen(); + return; + } + + var localUser = multiplayerClient.LocalUser; + + switch (localUser?.State) + { + default: + setGreen(); + break; + + case MultiplayerUserState.Spectating: + case MultiplayerUserState.Ready: + if (room?.Host?.Equals(localUser) == true) + setGreen(); + else + setYellow(); + + break; + } + + void setYellow() + { + BackgroundColour = colours.YellowDark; + Triangles.ColourDark = colours.YellowDark; + Triangles.ColourLight = colours.Yellow; + } + + void setGreen() + { + BackgroundColour = colours.Green; + Triangles.ColourDark = colours.Green; + Triangles.ColourLight = colours.GreenLight; + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (multiplayerClient != null) + multiplayerClient.RoomUpdated -= onRoomUpdated; + } } } } From efce471f0baacc69239aecdea135faf99586f974 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Mar 2022 19:05:28 +0900 Subject: [PATCH 02/43] Add countdown button + popover --- .../TestSceneMultiplayerReadyButton.cs | 18 +- .../TestSceneMultiplayerSpectateButton.cs | 33 ++-- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 181 +++++++++--------- .../Match/MultiplayerReadyButton.cs | 132 ++++++++++++- 4 files changed, 246 insertions(+), 118 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 7f7a4c9c5f..12b3b90fc4 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -8,6 +8,7 @@ using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Cursor; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Framework.Utils; @@ -55,15 +56,16 @@ namespace osu.Game.Tests.Visual.Multiplayer RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID }; - if (button != null) - Remove(button); - - Add(button = new MultiplayerReadyButton + Child = new PopoverContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(200, 50), - }); + RelativeSizeAxes = Axes.Both, + Child = button = new MultiplayerReadyButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(200, 50), + } + }; }); [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index 33ad0fd1de..13c9e021db 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -9,6 +9,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; @@ -56,23 +57,27 @@ namespace osu.Game.Tests.Visual.Multiplayer RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, }; - Child = new FillFlowContainer + Child = new PopoverContainer { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Children = new Drawable[] + RelativeSizeAxes = Axes.Both, + Child = new FillFlowContainer { - spectateButton = new MultiplayerSpectateButton + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(200, 50), - }, - readyButton = new MultiplayerReadyButton - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(200, 50), + spectateButton = new MultiplayerSpectateButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(200, 50), + }, + readyButton = new MultiplayerReadyButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(200, 50), + } } } }; diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index e297c90491..a382f65d84 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -12,6 +12,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; using osu.Game.Audio; @@ -100,122 +101,126 @@ namespace osu.Game.Screens.OnlinePlay.Match { sampleStart = audio.Samples.Get(@"SongSelect/confirm-selection"); - InternalChildren = new Drawable[] + InternalChild = new PopoverContainer { - beatmapAvailabilityTracker, - new MultiplayerRoomSounds(), - new GridContainer + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - RowDimensions = new[] + beatmapAvailabilityTracker, + new MultiplayerRoomSounds(), + new GridContainer { - new Dimension(), - new Dimension(GridSizeMode.Absolute, 50) - }, - Content = new[] - { - // Padded main content (drawable room + main content) - new Drawable[] + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] { - new Container + new Dimension(), + new Dimension(GridSizeMode.Absolute, 50) + }, + Content = new[] + { + // Padded main content (drawable room + main content) + new Drawable[] { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding + new Container { - Horizontal = WaveOverlayContainer.WIDTH_PADDING, - Bottom = 30 - }, - Children = new[] - { - mainContent = new GridContainer + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { - RelativeSizeAxes = Axes.Both, - RowDimensions = new[] + Horizontal = WaveOverlayContainer.WIDTH_PADDING, + Bottom = 30 + }, + Children = new[] + { + mainContent = new GridContainer { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.Absolute, 10) - }, - Content = new[] - { - new Drawable[] + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] { - new DrawableMatchRoom(Room, allowEdit) - { - OnEdit = () => settingsOverlay.Show(), - SelectedItem = { BindTarget = SelectedItem } - } + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, 10) }, - null, - new Drawable[] + Content = new[] { - new Container + new Drawable[] { - RelativeSizeAxes = Axes.Both, - Children = new[] + new DrawableMatchRoom(Room, allowEdit) { - new Container + OnEdit = () => settingsOverlay.Show(), + SelectedItem = { BindTarget = SelectedItem } + } + }, + null, + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new[] { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = 10, - Child = new Box + new Container { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"3e3a44") // Temporary. + Masking = true, + CornerRadius = 10, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex(@"3e3a44") // Temporary. + }, }, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(20), - Child = CreateMainContent(), - }, - new Container - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Child = userModsSelectOverlay = new UserModSelectOverlay + new Container { - SelectedMods = { BindTarget = UserMods }, - IsValidMod = _ => false - } - }, + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(20), + Child = CreateMainContent(), + }, + new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = userModsSelectOverlay = new UserModSelectOverlay + { + SelectedMods = { BindTarget = UserMods }, + IsValidMod = _ => false + } + }, + } } } } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + // Resolves 1px masking errors between the settings overlay and the room panel. + Padding = new MarginPadding(-1), + Child = settingsOverlay = CreateRoomSettingsOverlay(Room) } }, - new Container - { - RelativeSizeAxes = Axes.Both, - // Resolves 1px masking errors between the settings overlay and the room panel. - Padding = new MarginPadding(-1), - Child = settingsOverlay = CreateRoomSettingsOverlay(Room) - } }, }, - }, - // Footer - new Drawable[] - { - new Container + // Footer + new Drawable[] { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + new Container { - new Box + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"28242d") // Temporary. - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(5), - Child = CreateFooter() - }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex(@"28242d") // Temporary. + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(5), + Child = CreateFooter() + }, + } } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 920e23eaa1..4e53b40075 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -4,15 +4,24 @@ using System; using System.Diagnostics; using System.Linq; +using Humanizer; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; 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 osuTK; @@ -30,19 +39,43 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private Sample sampleReadyAll; private Sample sampleUnready; - private readonly ReadyButton readyButton; + private readonly BindableBool enabled = new BindableBool(); + private readonly CountdownButton countdownButton; private int countReady; private ScheduledDelegate readySampleDelegate; private IBindable operationInProgress; public MultiplayerReadyButton() { - InternalChild = readyButton = new ReadyButton + InternalChild = new GridContainer { RelativeSizeAxes = Axes.Both, - Size = Vector2.One, - Action = onReadyClick, - Enabled = { Value = true }, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + new ReadyButton + { + RelativeSizeAxes = Axes.Both, + Size = Vector2.One, + Action = onReadyClick, + Enabled = { BindTarget = enabled }, + }, + countdownButton = new CountdownButton + { + RelativeSizeAxes = Axes.Y, + Size = new Vector2(40, 1), + Alpha = 0, + Action = startCountdown, + Enabled = { BindTarget = enabled } + } + } + } }; } @@ -111,6 +144,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match }); } + private void startCountdown(TimeSpan duration) + { + } + private void endOperation() { clickOperation?.Dispose(); @@ -121,7 +158,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { if (Room == null) { - readyButton.Enabled.Value = false; + enabled.Value = false; return; } @@ -130,7 +167,19 @@ 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); - readyButton.Enabled.Value = + switch (localUser?.State) + { + default: + countdownButton.Alpha = 0; + break; + + case MultiplayerUserState.Spectating: + case MultiplayerUserState.Ready: + countdownButton.Alpha = Room.Host?.Equals(localUser) == true ? 1 : 0; + break; + } + + enabled.Value = Room.State == MultiplayerRoomState.Open && CurrentPlaylistItem.Value?.ID == Room.Settings.PlaylistItemId && !Room.Playlist.Single(i => i.ID == Room.Settings.PlaylistItemId).Expired @@ -138,7 +187,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match // When the local user is the host and spectating the match, the "start match" state should be enabled if any users are ready. if (localUser?.State == MultiplayerUserState.Spectating) - readyButton.Enabled.Value &= Room.Host?.Equals(localUser) == true && newCountReady > 0; + enabled.Value &= Room.Host?.Equals(localUser) == true && newCountReady > 0; if (newCountReady == countReady) return; @@ -269,5 +318,72 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match multiplayerClient.RoomUpdated -= onRoomUpdated; } } + + public class CountdownButton : IconButton, IHasPopover + { + private static readonly TimeSpan[] available_delays = + { + TimeSpan.FromSeconds(10), + TimeSpan.FromSeconds(30), + TimeSpan.FromMinutes(1), + TimeSpan.FromMinutes(2) + }; + + public new Action Action; + + private readonly Drawable background; + + public CountdownButton() + { + Icon = FontAwesome.Solid.CaretDown; + IconScale = new Vector2(0.6f); + + Add(background = new Box + { + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue + }); + + base.Action = this.ShowPopover; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + background.Colour = colours.Green; + } + + public Popover GetPopover() + { + var flow = new FillFlowContainer + { + Width = 200, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(2), + }; + + foreach (var duration in available_delays) + { + flow.Add(new PopoverButton + { + RelativeSizeAxes = Axes.X, + Text = $"Start match in {duration.Humanize()}", + BackgroundColour = background.Colour, + Action = () => + { + Action(duration); + this.HidePopover(); + } + }); + } + + return new OsuPopover { Child = flow }; + } + + public class PopoverButton : OsuButton + { + } + } } } From 3b938865a1d3fd4ee4d9972dd37a36f2365ae7df Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Mar 2022 19:14:46 +0900 Subject: [PATCH 03/43] Add room structure for countdown timers --- .../Countdown/CountdownChangedEvent.cs | 22 +++++++++++++++++ .../Countdown/MatchStartCountdownRequest.cs | 23 ++++++++++++++++++ .../Countdown/StopCountdownRequest.cs | 17 +++++++++++++ .../Online/Multiplayer/MatchServerEvent.cs | 5 ++++ .../Online/Multiplayer/MatchStartCountdown.cs | 17 +++++++++++++ .../TeamVersus/ChangeTeamRequest.cs | 1 + .../Online/Multiplayer/MatchUserRequest.cs | 6 ++++- .../Multiplayer/MultiplayerCountdown.cs | 24 +++++++++++++++++++ .../Online/Multiplayer/MultiplayerRoom.cs | 6 +++++ osu.Game/Online/SignalRWorkaroundTypes.cs | 5 ++++ 10 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Online/Multiplayer/Countdown/CountdownChangedEvent.cs create mode 100644 osu.Game/Online/Multiplayer/Countdown/MatchStartCountdownRequest.cs create mode 100644 osu.Game/Online/Multiplayer/Countdown/StopCountdownRequest.cs create mode 100644 osu.Game/Online/Multiplayer/MatchStartCountdown.cs create mode 100644 osu.Game/Online/Multiplayer/MultiplayerCountdown.cs diff --git a/osu.Game/Online/Multiplayer/Countdown/CountdownChangedEvent.cs b/osu.Game/Online/Multiplayer/Countdown/CountdownChangedEvent.cs new file mode 100644 index 0000000000..b067f3b235 --- /dev/null +++ b/osu.Game/Online/Multiplayer/Countdown/CountdownChangedEvent.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using MessagePack; + +namespace osu.Game.Online.Multiplayer.Countdown +{ + /// + /// Indicates a change to the 's countdown. + /// + [MessagePackObject] + public class CountdownChangedEvent : MatchServerEvent + { + /// + /// The new countdown. + /// + [Key(0)] + public MultiplayerCountdown? Countdown { get; set; } + } +} diff --git a/osu.Game/Online/Multiplayer/Countdown/MatchStartCountdownRequest.cs b/osu.Game/Online/Multiplayer/Countdown/MatchStartCountdownRequest.cs new file mode 100644 index 0000000000..04e7f506c2 --- /dev/null +++ b/osu.Game/Online/Multiplayer/Countdown/MatchStartCountdownRequest.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using MessagePack; + +#nullable enable + +namespace osu.Game.Online.Multiplayer.Countdown +{ + /// + /// A request for a countdown to start the match. + /// + [MessagePackObject] + public class MatchStartCountdownRequest : MatchUserRequest + { + /// + /// How long the countdown should last. + /// + [Key(0)] + public TimeSpan Delay { get; set; } + } +} diff --git a/osu.Game/Online/Multiplayer/Countdown/StopCountdownRequest.cs b/osu.Game/Online/Multiplayer/Countdown/StopCountdownRequest.cs new file mode 100644 index 0000000000..20a0e32734 --- /dev/null +++ b/osu.Game/Online/Multiplayer/Countdown/StopCountdownRequest.cs @@ -0,0 +1,17 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using MessagePack; + +namespace osu.Game.Online.Multiplayer.Countdown +{ + /// + /// Request to stop the current countdown. + /// + [MessagePackObject] + public class StopCountdownRequest : MatchUserRequest + { + } +} diff --git a/osu.Game/Online/Multiplayer/MatchServerEvent.cs b/osu.Game/Online/Multiplayer/MatchServerEvent.cs index 891fb2cc3b..4ce55e424d 100644 --- a/osu.Game/Online/Multiplayer/MatchServerEvent.cs +++ b/osu.Game/Online/Multiplayer/MatchServerEvent.cs @@ -1,8 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System; using MessagePack; +using osu.Game.Online.Multiplayer.Countdown; namespace osu.Game.Online.Multiplayer { @@ -11,6 +14,8 @@ namespace osu.Game.Online.Multiplayer /// [Serializable] [MessagePackObject] + // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types. + [Union(0, typeof(CountdownChangedEvent))] public abstract class MatchServerEvent { } diff --git a/osu.Game/Online/Multiplayer/MatchStartCountdown.cs b/osu.Game/Online/Multiplayer/MatchStartCountdown.cs new file mode 100644 index 0000000000..6c1cdd97d3 --- /dev/null +++ b/osu.Game/Online/Multiplayer/MatchStartCountdown.cs @@ -0,0 +1,17 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using MessagePack; + +namespace osu.Game.Online.Multiplayer +{ + /// + /// A which will start the match after ending. + /// + [MessagePackObject] + public class MatchStartCountdown : MultiplayerCountdown + { + } +} diff --git a/osu.Game/Online/Multiplayer/MatchTypes/TeamVersus/ChangeTeamRequest.cs b/osu.Game/Online/Multiplayer/MatchTypes/TeamVersus/ChangeTeamRequest.cs index 9c3b07049c..a26a2b3fc2 100644 --- a/osu.Game/Online/Multiplayer/MatchTypes/TeamVersus/ChangeTeamRequest.cs +++ b/osu.Game/Online/Multiplayer/MatchTypes/TeamVersus/ChangeTeamRequest.cs @@ -7,6 +7,7 @@ using MessagePack; namespace osu.Game.Online.Multiplayer.MatchTypes.TeamVersus { + [MessagePackObject] public class ChangeTeamRequest : MatchUserRequest { [Key(0)] diff --git a/osu.Game/Online/Multiplayer/MatchUserRequest.cs b/osu.Game/Online/Multiplayer/MatchUserRequest.cs index 8c6809e7f3..fa7bdd8afe 100644 --- a/osu.Game/Online/Multiplayer/MatchUserRequest.cs +++ b/osu.Game/Online/Multiplayer/MatchUserRequest.cs @@ -3,6 +3,7 @@ using System; using MessagePack; +using osu.Game.Online.Multiplayer.Countdown; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; namespace osu.Game.Online.Multiplayer @@ -12,7 +13,10 @@ namespace osu.Game.Online.Multiplayer /// [Serializable] [MessagePackObject] - [Union(0, typeof(ChangeTeamRequest))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types. + // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types. + [Union(0, typeof(ChangeTeamRequest))] + [Union(1, typeof(MatchStartCountdownRequest))] + [Union(2, typeof(StopCountdownRequest))] public abstract class MatchUserRequest { } diff --git a/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs b/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs new file mode 100644 index 0000000000..63bb47b295 --- /dev/null +++ b/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using System; +using MessagePack; + +namespace osu.Game.Online.Multiplayer +{ + /// + /// Describes the current countdown in a . + /// + [MessagePackObject] + [Union(0, typeof(MatchStartCountdown))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types. + public abstract class MultiplayerCountdown + { + /// + /// The time at which the countdown will end. + /// + [Key(0)] + public DateTimeOffset EndTime { get; set; } + } +} diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs index a60e70dab3..e215498ff9 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs @@ -54,6 +54,12 @@ namespace osu.Game.Online.Multiplayer [Key(6)] public IList Playlist { get; set; } = new List(); + /// + /// The currently-running countdown. + /// + [Key(7)] + public MultiplayerCountdown? Countdown { get; set; } + [JsonConstructor] [SerializationConstructor] public MultiplayerRoom(long roomId) diff --git a/osu.Game/Online/SignalRWorkaroundTypes.cs b/osu.Game/Online/SignalRWorkaroundTypes.cs index f69d23d81c..3e2b697337 100644 --- a/osu.Game/Online/SignalRWorkaroundTypes.cs +++ b/osu.Game/Online/SignalRWorkaroundTypes.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.Countdown; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; namespace osu.Game.Online @@ -18,8 +19,12 @@ namespace osu.Game.Online internal static readonly IReadOnlyList<(Type derivedType, Type baseType)> BASE_TYPE_MAPPING = new[] { (typeof(ChangeTeamRequest), typeof(MatchUserRequest)), + (typeof(MatchStartCountdownRequest), typeof(MatchUserRequest)), + (typeof(StopCountdownRequest), typeof(MatchUserRequest)), + (typeof(CountdownChangedEvent), typeof(MatchServerEvent)), (typeof(TeamVersusRoomState), typeof(MatchRoomState)), (typeof(TeamVersusUserState), typeof(MatchUserState)), + (typeof(MatchStartCountdown), typeof(MultiplayerCountdown)) }; } } From 72843a679783d6c762117398f36c6dfc26a71d2a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Mar 2022 19:26:42 +0900 Subject: [PATCH 04/43] Add support for starting/stopping countdowns --- .../TestSceneMultiplayerReadyButton.cs | 136 ++++++++++++++++++ .../Online/Multiplayer/MultiplayerClient.cs | 20 ++- .../OnlinePlay/Components/ReadyButton.cs | 12 +- .../Match/MultiplayerReadyButton.cs | 136 ++++++++++++++---- .../Multiplayer/TestMultiplayerClient.cs | 89 +++++++++++- 5 files changed, 354 insertions(+), 39 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 12b3b90fc4..b4cbc40403 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . 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(); + AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + ClickButtonWhenEnabled(); + AddStep("click the first countdown button", () => + { + var popoverButton = this.ChildrenOfType().First(); + InputManager.MoveMouseTo(popoverButton); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("countdown button not visible", () => !this.ChildrenOfType().Single().IsPresent); + AddStep("finish countdown", () => MultiplayerClient.FinishCountDown()); + AddUntilStep("match started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.WaitingForLoad); + } + + [Test] + public void TestCancelCountdown() + { + ClickButtonWhenEnabled(); + AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + ClickButtonWhenEnabled(); + AddStep("click the first countdown button", () => + { + var popoverButton = this.ChildrenOfType().First(); + InputManager.MoveMouseTo(popoverButton); + InputManager.Click(MouseButton.Left); + }); + + ClickButtonWhenEnabled(); + + 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(); + AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); + + ClickButtonWhenEnabled(); + 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().Single().IsPresent); + AddAssert("countdown button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); + + AddStep("add second user", () => MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" })); + AddAssert("countdown button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); + + AddStep("set second user ready", () => MultiplayerClient.ChangeUserState(2, MultiplayerUserState.Ready)); + AddAssert("countdown button enabled", () => this.ChildrenOfType().Single().Enabled.Value); + } + + [Test] + public void TestSpectatingDuringCountdownWithNoReadyUsersCancelsCountdown() + { + ClickButtonWhenEnabled(); + AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + ClickButtonWhenEnabled(); + AddStep("click the first countdown button", () => + { + var popoverButton = this.ChildrenOfType().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(); + AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + ClickButtonWhenEnabled(); + AddStep("click the first countdown button", () => + { + var popoverButton = this.ChildrenOfType().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().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(); + AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); + AddAssert("countdown still active", () => MultiplayerClient.Room?.Countdown != null); + } + [Test] public void TestDeletedBeatmapDisableReady() { diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index a56cc7f8d6..2d5496c5c1 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -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; } diff --git a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs index 9822ceaaf6..79cf5c7236 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs @@ -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 availability; + protected readonly IBindable Availability = new Bindable(); [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; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 4e53b40075..f9f070f17a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -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 diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 6dc5159b6f..a1ae1aa171 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -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() From 04f4e81852b846caee2b70e9cee1b47ac64dd7bc Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 18 Mar 2022 21:05:19 +0900 Subject: [PATCH 05/43] Rename start countdown request --- .../Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs | 4 ++-- ...StartCountdownRequest.cs => StartMatchCountdownRequest.cs} | 2 +- osu.Game/Online/Multiplayer/MatchUserRequest.cs | 2 +- osu.Game/Online/SignalRWorkaroundTypes.cs | 2 +- .../OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs | 2 +- osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) rename osu.Game/Online/Multiplayer/Countdown/{MatchStartCountdownRequest.cs => StartMatchCountdownRequest.cs} (89%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index b4cbc40403..fc47861576 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -117,7 +117,7 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.TransferHost(2); }); - AddStep("start with countdown", () => MultiplayerClient.SendMatchRequest(new MatchStartCountdownRequest { Delay = TimeSpan.FromMinutes(2) })); + AddStep("start with countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Delay = TimeSpan.FromMinutes(2) })); ClickButtonWhenEnabled(); AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); @@ -193,7 +193,7 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.TransferHost(2); }); - AddStep("start countdown", () => MultiplayerClient.SendMatchRequest(new MatchStartCountdownRequest { Delay = TimeSpan.FromMinutes(1) })); + AddStep("start countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Delay = TimeSpan.FromMinutes(1) })); AddUntilStep("countdown started", () => MultiplayerClient.Room?.Countdown != null); AddStep("transfer host to local user", () => MultiplayerClient.TransferHost(API.LocalUser.Value.OnlineID)); diff --git a/osu.Game/Online/Multiplayer/Countdown/MatchStartCountdownRequest.cs b/osu.Game/Online/Multiplayer/Countdown/StartMatchCountdownRequest.cs similarity index 89% rename from osu.Game/Online/Multiplayer/Countdown/MatchStartCountdownRequest.cs rename to osu.Game/Online/Multiplayer/Countdown/StartMatchCountdownRequest.cs index 04e7f506c2..9e6967af9d 100644 --- a/osu.Game/Online/Multiplayer/Countdown/MatchStartCountdownRequest.cs +++ b/osu.Game/Online/Multiplayer/Countdown/StartMatchCountdownRequest.cs @@ -12,7 +12,7 @@ namespace osu.Game.Online.Multiplayer.Countdown /// A request for a countdown to start the match. /// [MessagePackObject] - public class MatchStartCountdownRequest : MatchUserRequest + public class StartMatchCountdownRequest : MatchUserRequest { /// /// How long the countdown should last. diff --git a/osu.Game/Online/Multiplayer/MatchUserRequest.cs b/osu.Game/Online/Multiplayer/MatchUserRequest.cs index fa7bdd8afe..888b55e428 100644 --- a/osu.Game/Online/Multiplayer/MatchUserRequest.cs +++ b/osu.Game/Online/Multiplayer/MatchUserRequest.cs @@ -15,7 +15,7 @@ namespace osu.Game.Online.Multiplayer [MessagePackObject] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types. [Union(0, typeof(ChangeTeamRequest))] - [Union(1, typeof(MatchStartCountdownRequest))] + [Union(1, typeof(StartMatchCountdownRequest))] [Union(2, typeof(StopCountdownRequest))] public abstract class MatchUserRequest { diff --git a/osu.Game/Online/SignalRWorkaroundTypes.cs b/osu.Game/Online/SignalRWorkaroundTypes.cs index 3e2b697337..156f916cef 100644 --- a/osu.Game/Online/SignalRWorkaroundTypes.cs +++ b/osu.Game/Online/SignalRWorkaroundTypes.cs @@ -19,7 +19,7 @@ namespace osu.Game.Online internal static readonly IReadOnlyList<(Type derivedType, Type baseType)> BASE_TYPE_MAPPING = new[] { (typeof(ChangeTeamRequest), typeof(MatchUserRequest)), - (typeof(MatchStartCountdownRequest), typeof(MatchUserRequest)), + (typeof(StartMatchCountdownRequest), typeof(MatchUserRequest)), (typeof(StopCountdownRequest), typeof(MatchUserRequest)), (typeof(CountdownChangedEvent), typeof(MatchServerEvent)), (typeof(TeamVersusRoomState), typeof(MatchRoomState)), diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index f9f070f17a..381c5ea712 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -161,7 +161,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match Debug.Assert(clickOperation == null); clickOperation = ongoingOperationTracker.BeginOperation(); - Client.SendMatchRequest(new MatchStartCountdownRequest { Delay = duration }).ContinueWith(_ => endOperation()); + Client.SendMatchRequest(new StartMatchCountdownRequest { Delay = duration }).ContinueWith(_ => endOperation()); } private void endOperation() diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index a1ae1aa171..4066cd0c4a 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -309,7 +309,7 @@ namespace osu.Game.Tests.Visual.Multiplayer switch (request) { - case MatchStartCountdownRequest matchCountdownRequest: + case StartMatchCountdownRequest matchCountdownRequest: countdownStopSource?.Cancel(); var stopSource = countdownStopSource = new CancellationTokenSource(); From 4630aa15cc06a1efd1df0fe1486777c50d6999db Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 22 Mar 2022 12:54:10 +0900 Subject: [PATCH 06/43] Apply refactorings according to reviews --- .../Match/MultiplayerReadyButton.cs | 41 ++++++------------- .../Multiplayer/TestMultiplayerClient.cs | 8 +--- 2 files changed, 13 insertions(+), 36 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 381c5ea712..4c4cc87f6d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -282,12 +282,12 @@ 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)"; if (room.Countdown != null) { + string countdownText = $"Starting in {room.Countdown.EndTime - DateTimeOffset.Now:mm\\:ss}"; + switch (localUser?.State) { default: @@ -329,37 +329,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match var localUser = multiplayerClient.LocalUser; - if (room.Countdown != null) + switch (localUser?.State) { - switch (localUser?.State) - { - default: - setGreen(); - break; + default: + setGreen(); + break; - case MultiplayerUserState.Spectating: - case MultiplayerUserState.Ready: + case MultiplayerUserState.Spectating: + case MultiplayerUserState.Ready: + if (room?.Host?.Equals(localUser) == true && room.Countdown == null) + setGreen(); + else 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; - } + break; } void setYellow() diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 4066cd0c4a..309ca6ca58 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -322,13 +322,7 @@ namespace osu.Game.Tests.Visual.Multiplayer async Task start() { - try - { - await lastCountdownTask; - } - catch (OperationCanceledException) - { - } + await lastCountdownTask; Schedule(() => { From 2c4a6c246592cb8cb142aeb8a0704d34715cd30d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Mar 2022 16:46:42 +0900 Subject: [PATCH 07/43] Add missing async safeties to new tests --- .../Visual/Multiplayer/TestSceneMultiplayer.cs | 12 ++++++------ .../Multiplayer/TestSceneMultiplayerReadyButton.cs | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index e38da96bd5..6300dfa381 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -424,7 +424,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("Beatmap doesn't match current item", () => Beatmap.Value.BeatmapInfo.OnlineID != multiplayerClient.Room?.Playlist.First().BeatmapID); - AddStep("start match externally", () => multiplayerClient.StartMatch()); + AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely()); AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player); @@ -462,7 +462,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("Ruleset doesn't match current item", () => Ruleset.Value.OnlineID != multiplayerClient.Room?.Playlist.First().RulesetID); - AddStep("start match externally", () => multiplayerClient.StartMatch()); + AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely()); AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player); @@ -500,7 +500,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("Mods don't match current item", () => !SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym))); - AddStep("start match externally", () => multiplayerClient.StartMatch()); + AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely()); AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player); @@ -535,7 +535,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for spectating user state", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); - AddStep("start match externally", () => multiplayerClient.StartMatch()); + AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely()); AddAssert("play not started", () => multiplayerComponents.IsCurrentScreen()); } @@ -568,7 +568,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for spectating user state", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); - AddStep("start match externally", () => multiplayerClient.StartMatch()); + AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely()); AddStep("restore beatmap", () => { @@ -883,7 +883,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("start match by other user", () => { multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Ready); - multiplayerClient.StartMatch(); + multiplayerClient.StartMatch().WaitSafely(); }); AddUntilStep("wait for loading", () => multiplayerClient.Room?.State == MultiplayerRoomState.WaitingForLoad); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index fc47861576..0315e5dfe4 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -117,7 +117,7 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.TransferHost(2); }); - AddStep("start with countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Delay = TimeSpan.FromMinutes(2) })); + AddStep("start with countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Delay = TimeSpan.FromMinutes(2) }).WaitSafely()); ClickButtonWhenEnabled(); AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); @@ -193,7 +193,7 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.TransferHost(2); }); - AddStep("start countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Delay = TimeSpan.FromMinutes(1) })); + AddStep("start countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Delay = TimeSpan.FromMinutes(1) }).WaitSafely()); AddUntilStep("countdown started", () => MultiplayerClient.Room?.Countdown != null); AddStep("transfer host to local user", () => MultiplayerClient.TransferHost(API.LocalUser.Value.OnlineID)); From 483fb84b56bda3e3d6633c63d6daf2d74cd4e5ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Mar 2022 16:50:13 +0900 Subject: [PATCH 08/43] Fix typo in `FinishCountdown` method --- .../Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs | 6 +++--- osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 0315e5dfe4..7bf03caa88 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -85,7 +85,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddAssert("countdown button not visible", () => !this.ChildrenOfType().Single().IsPresent); - AddStep("finish countdown", () => MultiplayerClient.FinishCountDown()); + AddStep("finish countdown", () => MultiplayerClient.FinishCountdown()); AddUntilStep("match started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.WaitingForLoad); } @@ -104,7 +104,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); - AddStep("finish countdown", () => MultiplayerClient.FinishCountDown()); + AddStep("finish countdown", () => MultiplayerClient.FinishCountdown()); AddUntilStep("match not started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); } @@ -158,7 +158,7 @@ namespace osu.Game.Tests.Visual.Multiplayer 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()); + AddStep("finish countdown", () => MultiplayerClient.FinishCountdown()); AddUntilStep("match not started", () => MultiplayerClient.Room?.State == MultiplayerRoomState.Open); } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 309ca6ca58..2b03017905 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -300,7 +300,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private CancellationTokenSource? countdownStopSource; private Task countdownTask = Task.CompletedTask; - public void FinishCountDown() => countdownFinishSource?.Cancel(); + public void FinishCountdown() => countdownFinishSource?.Cancel(); public override async Task SendMatchRequest(MatchUserRequest request) { From 9138aaf78039b39a7fcba9c2b9ada333c5346ae5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 23 Mar 2022 10:37:53 +0900 Subject: [PATCH 09/43] Split MultiplayerReadyButton --- .../Visual/Multiplayer/QueueModeTestScene.cs | 4 +- .../TestSceneAllPlayersQueueMode.cs | 8 +- ...utton.cs => TestSceneMatchStartControl.cs} | 80 +++--- .../Multiplayer/TestSceneMultiplayer.cs | 1 + .../TestSceneMultiplayerMatchSubScreen.cs | 2 +- .../TestSceneMultiplayerSpectateButton.cs | 6 +- .../Multiplayer/Match/CountdownButton.cs | 87 +++++++ ...yerReadyButton.cs => MatchStartControl.cs} | 230 +----------------- .../Match/MultiplayerMatchFooter.cs | 2 +- .../Multiplayer/Match/ReadyButton.cs | 162 ++++++++++++ 10 files changed, 303 insertions(+), 279 deletions(-) rename osu.Game.Tests/Visual/Multiplayer/{TestSceneMultiplayerReadyButton.cs => TestSceneMatchStartControl.cs} (79%) create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs rename osu.Game/Screens/OnlinePlay/Multiplayer/Match/{MultiplayerReadyButton.cs => MatchStartControl.cs} (51%) create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Match/ReadyButton.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index df8f63f6ed..fd43674b3b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -95,10 +95,10 @@ namespace osu.Game.Tests.Visual.Multiplayer protected void RunGameplay() { AddUntilStep("wait for idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for player", () => multiplayerComponents.CurrentScreen is Player player && player.IsLoaded); AddStep("exit player", () => multiplayerComponents.MultiplayerScreen.MakeCurrent()); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs index 266ac60168..582dacb332 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs @@ -102,10 +102,10 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID); AddUntilStep("wait for idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for player", () => CurrentScreen is Player player && player.IsLoaded); AddAssert("ruleset is correct", () => ((Player)CurrentScreen).Ruleset.Value.Equals(new OsuRuleset().RulesetInfo)); @@ -119,10 +119,10 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID); AddUntilStep("wait for idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for player", () => CurrentScreen is Player player && player.IsLoaded); AddAssert("mods are correct", () => !((Player)CurrentScreen).Mods.Value.Any()); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs similarity index 79% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs rename to osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs index 7bf03caa88..4e54740a69 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs @@ -27,9 +27,9 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneMultiplayerReadyButton : MultiplayerTestScene + public class TestSceneMatchStartControl : MultiplayerTestScene { - private MultiplayerReadyButton button; + private MatchStartControl control; private BeatmapSetInfo importedSet; private readonly Bindable selectedItem = new Bindable(); @@ -62,7 +62,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Child = new PopoverContainer { RelativeSizeAxes = Axes.Both, - Child = button = new MultiplayerReadyButton + Child = control = new MatchStartControl { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -74,17 +74,17 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestStartWithCountdown() { - ClickButtonWhenEnabled(); - AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); + AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { - var popoverButton = this.ChildrenOfType().First(); + var popoverButton = this.ChildrenOfType().First(); InputManager.MoveMouseTo(popoverButton); InputManager.Click(MouseButton.Left); }); - AddAssert("countdown button not visible", () => !this.ChildrenOfType().Single().IsPresent); + AddAssert("countdown button not visible", () => !this.ChildrenOfType().Single().IsPresent); AddStep("finish countdown", () => MultiplayerClient.FinishCountdown()); AddUntilStep("match started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.WaitingForLoad); } @@ -92,17 +92,17 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestCancelCountdown() { - ClickButtonWhenEnabled(); - AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); + AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { - var popoverButton = this.ChildrenOfType().First(); + var popoverButton = this.ChildrenOfType().First(); InputManager.MoveMouseTo(popoverButton); InputManager.Click(MouseButton.Left); }); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddStep("finish countdown", () => MultiplayerClient.FinishCountdown()); AddUntilStep("match not started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); @@ -119,10 +119,10 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("start with countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Delay = TimeSpan.FromMinutes(2) }).WaitSafely()); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle); } @@ -132,25 +132,25 @@ namespace osu.Game.Tests.Visual.Multiplayer 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().Single().IsPresent); - AddAssert("countdown button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); + AddAssert("countdown button is visible", () => this.ChildrenOfType().Single().IsPresent); + AddAssert("countdown button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); AddStep("add second user", () => MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" })); - AddAssert("countdown button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); + AddAssert("countdown button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); AddStep("set second user ready", () => MultiplayerClient.ChangeUserState(2, MultiplayerUserState.Ready)); - AddAssert("countdown button enabled", () => this.ChildrenOfType().Single().Enabled.Value); + AddAssert("countdown button enabled", () => this.ChildrenOfType().Single().Enabled.Value); } [Test] public void TestSpectatingDuringCountdownWithNoReadyUsersCancelsCountdown() { - ClickButtonWhenEnabled(); - AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); + AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { - var popoverButton = this.ChildrenOfType().First(); + var popoverButton = this.ChildrenOfType().First(); InputManager.MoveMouseTo(popoverButton); InputManager.Click(MouseButton.Left); }); @@ -168,12 +168,12 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("add second user", () => MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" })); AddStep("set second user ready", () => MultiplayerClient.ChangeUserState(2, MultiplayerUserState.Ready)); - ClickButtonWhenEnabled(); - AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); + AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { - var popoverButton = this.ChildrenOfType().First(); + var popoverButton = this.ChildrenOfType().First(); InputManager.MoveMouseTo(popoverButton); InputManager.Click(MouseButton.Left); }); @@ -181,7 +181,7 @@ namespace osu.Game.Tests.Visual.Multiplayer 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().Single().Enabled.Value); + AddAssert("ready button enabled", () => this.ChildrenOfType().Single().Enabled.Value); } [Test] @@ -199,7 +199,7 @@ namespace osu.Game.Tests.Visual.Multiplayer 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(); + ClickButtonWhenEnabled(); AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); AddAssert("countdown still active", () => MultiplayerClient.Room?.Countdown != null); } @@ -211,7 +211,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("ensure ready button enabled", () => { - readyButton = button.ChildrenOfType().Single(); + readyButton = control.ChildrenOfType().Single(); return readyButton.Enabled.Value; }); @@ -230,10 +230,10 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.TransferHost(2); }); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle); } @@ -249,7 +249,7 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }); }); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); verifyGameplayStartFlow(); @@ -264,7 +264,7 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.TransferHost(2); }); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddStep("make user host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0)); verifyGameplayStartFlow(); @@ -279,14 +279,14 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }); }); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); AddStep("transfer host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[1].UserID ?? 0)); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is idle (match not started)", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle); - AddAssert("ready button enabled", () => button.ChildrenOfType().Single().Enabled.Value); + AddAssert("ready button enabled", () => control.ChildrenOfType().Single().Enabled.Value); } [TestCase(true)] @@ -304,7 +304,7 @@ namespace osu.Game.Tests.Visual.Multiplayer if (!isHost) AddStep("transfer host", () => MultiplayerClient.TransferHost(2)); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddRepeatStep("change user ready state", () => { @@ -322,7 +322,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private void verifyGameplayStartFlow() { AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user waiting for load", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad); AddStep("finish gameplay", () => @@ -331,7 +331,7 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.ChangeUserState(MultiplayerClient.Room?.Users[0].UserID ?? 0, MultiplayerUserState.FinishedPlay); }); - AddUntilStep("ready button enabled", () => button.ChildrenOfType().Single().Enabled.Value); + AddUntilStep("ready button enabled", () => control.ChildrenOfType().Single().Enabled.Value); } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 6300dfa381..d0765fc4b3 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -41,6 +41,7 @@ using osu.Game.Screens.Ranking; using osu.Game.Screens.Spectate; using osu.Game.Tests.Resources; using osuTK.Input; +using ReadyButton = osu.Game.Screens.OnlinePlay.Components.ReadyButton; namespace osu.Game.Tests.Visual.Multiplayer { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index a51d4678fe..850a115f4c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -145,7 +145,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for spectating user state", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("match started", () => MultiplayerClient.Room?.State == MultiplayerRoomState.WaitingForLoad); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index 13c9e021db..07ac580276 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public class TestSceneMultiplayerSpectateButton : MultiplayerTestScene { private MultiplayerSpectateButton spectateButton; - private MultiplayerReadyButton readyButton; + private MatchStartControl startControl; private readonly Bindable selectedItem = new Bindable(); @@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Origin = Anchor.Centre, Size = new Vector2(200, 50), }, - readyButton = new MultiplayerReadyButton + startControl = new MatchStartControl { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -146,6 +146,6 @@ namespace osu.Game.Tests.Visual.Multiplayer => AddUntilStep($"spectate button {(shouldBeEnabled ? "is" : "is not")} enabled", () => spectateButton.ChildrenOfType().Single().Enabled.Value == shouldBeEnabled); private void assertReadyButtonEnablement(bool shouldBeEnabled) - => AddUntilStep($"ready button {(shouldBeEnabled ? "is" : "is not")} enabled", () => readyButton.ChildrenOfType().Single().Enabled.Value == shouldBeEnabled); + => AddUntilStep($"ready button {(shouldBeEnabled ? "is" : "is not")} enabled", () => startControl.ChildrenOfType().Single().Enabled.Value == shouldBeEnabled); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs new file mode 100644 index 0000000000..e598f6670c --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs @@ -0,0 +1,87 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using Humanizer; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match +{ + public class CountdownButton : IconButton, IHasPopover + { + private static readonly TimeSpan[] available_delays = + { + TimeSpan.FromSeconds(10), + TimeSpan.FromSeconds(30), + TimeSpan.FromMinutes(1), + TimeSpan.FromMinutes(2) + }; + + public new Action Action; + + private readonly Drawable background; + + public CountdownButton() + { + Icon = FontAwesome.Solid.CaretDown; + IconScale = new Vector2(0.6f); + + Add(background = new Box + { + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue + }); + + base.Action = this.ShowPopover; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + background.Colour = colours.Green; + } + + public Popover GetPopover() + { + var flow = new FillFlowContainer + { + Width = 200, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(2), + }; + + foreach (var duration in available_delays) + { + flow.Add(new PopoverButton + { + RelativeSizeAxes = Axes.X, + Text = $"Start match in {duration.Humanize()}", + BackgroundColour = background.Colour, + Action = () => + { + Action(duration); + this.HidePopover(); + } + }); + } + + return new OsuPopover { Child = flow }; + } + + public class PopoverButton : OsuButton + { + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs similarity index 51% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs index 4c4cc87f6d..d97ac601d5 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs @@ -4,32 +4,21 @@ using System; using System.Diagnostics; using System.Linq; -using Humanizer; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -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 { - public class MultiplayerReadyButton : MultiplayerRoomComposite + public class MatchStartControl : MultiplayerRoomComposite { [Resolved] private OngoingOperationTracker ongoingOperationTracker { get; set; } @@ -47,7 +36,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private ScheduledDelegate readySampleDelegate; private IBindable operationInProgress; - public MultiplayerReadyButton() + public MatchStartControl() { InternalChild = new GridContainer { @@ -231,220 +220,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match countReady = newCountReady; }); } - - public class ReadyButton : Components.ReadyButton - { - public new Triangles Triangles => base.Triangles; - - [Resolved] - private MultiplayerClient multiplayerClient { get; set; } - - [Resolved] - private OsuColour colours { get; set; } - - [CanBeNull] - private MultiplayerRoom room => multiplayerClient.Room; - - protected override void LoadComplete() - { - base.LoadComplete(); - - multiplayerClient.RoomUpdated += () => Scheduler.AddOnce(onRoomUpdated); - onRoomUpdated(); - } - - protected override void Update() - { - base.Update(); - - if (room?.Countdown != null) - { - // Update the countdown timer. - onRoomUpdated(); - } - } - - private void onRoomUpdated() - { - updateButtonText(); - updateButtonColour(); - } - - private void updateButtonText() - { - if (room == null) - { - Text = "Ready"; - return; - } - - var localUser = multiplayerClient.LocalUser; - - int countReady = room.Users.Count(u => u.State == MultiplayerUserState.Ready); - int countTotal = room.Users.Count(u => u.State != MultiplayerUserState.Spectating); - string countText = $"({countReady} / {countTotal} ready)"; - - if (room.Countdown != null) - { - string countdownText = $"Starting in {room.Countdown.EndTime - DateTimeOffset.Now:mm\\:ss}"; - - switch (localUser?.State) - { - default: - Text = $"Ready ({countdownText.ToLowerInvariant()})"; - break; - - case MultiplayerUserState.Spectating: - case MultiplayerUserState.Ready: - Text = $"{countdownText} {countText}"; - break; - } - } - else - { - switch (localUser?.State) - { - default: - Text = "Ready"; - break; - - case MultiplayerUserState.Spectating: - case MultiplayerUserState.Ready: - Text = room.Host?.Equals(localUser) == true - ? $"Start match {countText}" - : $"Waiting for host... {countText}"; - - break; - } - } - } - - private void updateButtonColour() - { - if (room == null) - { - setGreen(); - return; - } - - var localUser = multiplayerClient.LocalUser; - - switch (localUser?.State) - { - default: - setGreen(); - break; - - case MultiplayerUserState.Spectating: - case MultiplayerUserState.Ready: - if (room?.Host?.Equals(localUser) == true && room.Countdown == null) - setGreen(); - else - setYellow(); - - break; - } - - void setYellow() - { - BackgroundColour = colours.YellowDark; - Triangles.ColourDark = colours.YellowDark; - Triangles.ColourLight = colours.Yellow; - } - - void setGreen() - { - BackgroundColour = colours.Green; - Triangles.ColourDark = colours.Green; - Triangles.ColourLight = colours.GreenLight; - } - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - 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 - { - private static readonly TimeSpan[] available_delays = - { - TimeSpan.FromSeconds(10), - TimeSpan.FromSeconds(30), - TimeSpan.FromMinutes(1), - TimeSpan.FromMinutes(2) - }; - - public new Action Action; - - private readonly Drawable background; - - public CountdownButton() - { - Icon = FontAwesome.Solid.CaretDown; - IconScale = new Vector2(0.6f); - - Add(background = new Box - { - RelativeSizeAxes = Axes.Both, - Depth = float.MaxValue - }); - - base.Action = this.ShowPopover; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - background.Colour = colours.Green; - } - - public Popover GetPopover() - { - var flow = new FillFlowContainer - { - Width = 200, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(2), - }; - - foreach (var duration in available_delays) - { - flow.Add(new PopoverButton - { - RelativeSizeAxes = Axes.X, - Text = $"Start match in {duration.Humanize()}", - BackgroundColour = background.Colour, - Action = () => - { - Action(duration); - this.HidePopover(); - } - }); - } - - return new OsuPopover { Child = flow }; - } - - public class PopoverButton : OsuButton - { - } - } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs index b4fce5903b..a07c95bca8 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs @@ -28,7 +28,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match RelativeSizeAxes = Axes.Both, }, null, - new MultiplayerReadyButton + new MatchStartControl { RelativeSizeAxes = Axes.Both, }, diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ReadyButton.cs new file mode 100644 index 0000000000..8e3a9f9349 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ReadyButton.cs @@ -0,0 +1,162 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Backgrounds; +using osu.Game.Online.Multiplayer; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match +{ + public class ReadyButton : Components.ReadyButton + { + public new Triangles Triangles => base.Triangles; + + [Resolved] + private MultiplayerClient multiplayerClient { get; set; } + + [Resolved] + private OsuColour colours { get; set; } + + [CanBeNull] + private MultiplayerRoom room => multiplayerClient.Room; + + protected override void LoadComplete() + { + base.LoadComplete(); + + multiplayerClient.RoomUpdated += () => Scheduler.AddOnce(onRoomUpdated); + onRoomUpdated(); + } + + protected override void Update() + { + base.Update(); + + if (room?.Countdown != null) + { + // Update the countdown timer. + onRoomUpdated(); + } + } + + private void onRoomUpdated() + { + updateButtonText(); + updateButtonColour(); + } + + private void updateButtonText() + { + if (room == null) + { + Text = "Ready"; + return; + } + + var localUser = multiplayerClient.LocalUser; + + int countReady = room.Users.Count(u => u.State == MultiplayerUserState.Ready); + int countTotal = room.Users.Count(u => u.State != MultiplayerUserState.Spectating); + string countText = $"({countReady} / {countTotal} ready)"; + + if (room.Countdown != null) + { + string countdownText = $"Starting in {room.Countdown.EndTime - DateTimeOffset.Now:mm\\:ss}"; + + switch (localUser?.State) + { + default: + Text = $"Ready ({countdownText.ToLowerInvariant()})"; + break; + + case MultiplayerUserState.Spectating: + case MultiplayerUserState.Ready: + Text = $"{countdownText} {countText}"; + break; + } + } + else + { + switch (localUser?.State) + { + default: + Text = "Ready"; + break; + + case MultiplayerUserState.Spectating: + case MultiplayerUserState.Ready: + Text = room.Host?.Equals(localUser) == true + ? $"Start match {countText}" + : $"Waiting for host... {countText}"; + + break; + } + } + } + + private void updateButtonColour() + { + if (room == null) + { + setGreen(); + return; + } + + var localUser = multiplayerClient.LocalUser; + + switch (localUser?.State) + { + default: + setGreen(); + break; + + case MultiplayerUserState.Spectating: + case MultiplayerUserState.Ready: + if (room?.Host?.Equals(localUser) == true && room.Countdown == null) + setGreen(); + else + setYellow(); + + break; + } + + void setYellow() + { + BackgroundColour = colours.YellowDark; + Triangles.ColourDark = colours.YellowDark; + Triangles.ColourLight = colours.Yellow; + } + + void setGreen() + { + BackgroundColour = colours.Green; + Triangles.ColourDark = colours.Green; + Triangles.ColourLight = colours.GreenLight; + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + 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; + } + } + } +} From 6b712be97d08de140bd6d3b0cb81d8d90412194b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 23 Mar 2022 10:40:05 +0900 Subject: [PATCH 10/43] Remove PopoverButton class --- .../Visual/Multiplayer/TestSceneMatchStartControl.cs | 8 ++++---- .../OnlinePlay/Multiplayer/Match/CountdownButton.cs | 6 +----- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs index 4e54740a69..b98676e737 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs @@ -79,7 +79,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { - var popoverButton = this.ChildrenOfType().First(); + var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); InputManager.MoveMouseTo(popoverButton); InputManager.Click(MouseButton.Left); }); @@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { - var popoverButton = this.ChildrenOfType().First(); + var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); InputManager.MoveMouseTo(popoverButton); InputManager.Click(MouseButton.Left); }); @@ -150,7 +150,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { - var popoverButton = this.ChildrenOfType().First(); + var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); InputManager.MoveMouseTo(popoverButton); InputManager.Click(MouseButton.Left); }); @@ -173,7 +173,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { - var popoverButton = this.ChildrenOfType().First(); + var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); InputManager.MoveMouseTo(popoverButton); InputManager.Click(MouseButton.Left); }); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs index e598f6670c..e37168bf25 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs @@ -64,7 +64,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match foreach (var duration in available_delays) { - flow.Add(new PopoverButton + flow.Add(new OsuButton { RelativeSizeAxes = Axes.X, Text = $"Start match in {duration.Humanize()}", @@ -79,9 +79,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match return new OsuPopover { Child = flow }; } - - public class PopoverButton : OsuButton - { - } } } From d4ad4ac9db8c76be1cd9545944dca12e582d41f9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 23 Mar 2022 10:50:05 +0900 Subject: [PATCH 11/43] Limit countdown updates to once per second --- .../Multiplayer/Match/ReadyButton.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ReadyButton.cs index 8e3a9f9349..b37d990466 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ReadyButton.cs @@ -6,6 +6,7 @@ using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Localisation; +using osu.Framework.Threading; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Online.Multiplayer; @@ -33,21 +34,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match onRoomUpdated(); } - protected override void Update() - { - base.Update(); - - if (room?.Countdown != null) - { - // Update the countdown timer. - onRoomUpdated(); - } - } + private ScheduledDelegate countdownUpdateDelegate; private void onRoomUpdated() { updateButtonText(); updateButtonColour(); + + if (room?.Countdown != null) + countdownUpdateDelegate ??= Scheduler.AddDelayed(updateButtonText, 1000, true); + else + { + countdownUpdateDelegate?.Cancel(); + countdownUpdateDelegate = null; + } } private void updateButtonText() From f7c004720648ae8e296f3c1d2b2f49612a29f285 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 23 Mar 2022 15:19:43 +0900 Subject: [PATCH 12/43] Send time remaining in countdowns instead --- .../Multiplayer/MultiplayerCountdown.cs | 8 +++++-- .../Multiplayer/Match/ReadyButton.cs | 23 +++++++++++++++---- .../Multiplayer/TestMultiplayerClient.cs | 2 +- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs b/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs index 63bb47b295..81190e64c9 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs @@ -5,6 +5,7 @@ using System; using MessagePack; +using osu.Game.Online.Multiplayer.Countdown; namespace osu.Game.Online.Multiplayer { @@ -16,9 +17,12 @@ namespace osu.Game.Online.Multiplayer public abstract class MultiplayerCountdown { /// - /// The time at which the countdown will end. + /// The amount of time remaining in the countdown. /// + /// + /// This is only sent once from the server upon initial retrieval of the or via a . + /// [Key(0)] - public DateTimeOffset EndTime { get; set; } + public TimeSpan TimeRemaining { get; set; } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ReadyButton.cs index b37d990466..007e055d8c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ReadyButton.cs @@ -34,12 +34,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match onRoomUpdated(); } + private MultiplayerCountdown countdown; + private DateTimeOffset countdownReceivedTime; private ScheduledDelegate countdownUpdateDelegate; private void onRoomUpdated() { - updateButtonText(); - updateButtonColour(); + if (countdown == null && room?.Countdown != null) + countdownReceivedTime = DateTimeOffset.Now; + + countdown = room?.Countdown; if (room?.Countdown != null) countdownUpdateDelegate ??= Scheduler.AddDelayed(updateButtonText, 1000, true); @@ -48,6 +52,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match countdownUpdateDelegate?.Cancel(); countdownUpdateDelegate = null; } + + updateButtonText(); + updateButtonColour(); } private void updateButtonText() @@ -64,9 +71,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match int countTotal = room.Users.Count(u => u.State != MultiplayerUserState.Spectating); string countText = $"({countReady} / {countTotal} ready)"; - if (room.Countdown != null) + if (countdown != null) { - string countdownText = $"Starting in {room.Countdown.EndTime - DateTimeOffset.Now:mm\\:ss}"; + TimeSpan timeElapsed = DateTimeOffset.Now - countdownReceivedTime; + TimeSpan countdownRemaining; + + if (timeElapsed > countdown.TimeRemaining) + countdownRemaining = TimeSpan.Zero; + else + countdownRemaining = countdown.TimeRemaining - timeElapsed; + + string countdownText = $"Starting in {countdownRemaining:mm\\:ss}"; switch (localUser?.State) { diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 2b03017905..938147e05e 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -315,7 +315,7 @@ namespace osu.Game.Tests.Visual.Multiplayer 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 }; + var countdown = new MatchStartCountdown { TimeRemaining = matchCountdownRequest.Delay }; Task lastCountdownTask = countdownTask; countdownTask = start(); From a83a90e675f39a1646b8091c6da4c7eaeefe1d9d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 23 Mar 2022 15:21:16 +0900 Subject: [PATCH 13/43] Rename countdown Delay -> Duration --- .../Visual/Multiplayer/TestSceneMatchStartControl.cs | 4 ++-- .../Multiplayer/Countdown/StartMatchCountdownRequest.cs | 2 +- .../Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs | 2 +- osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs index b98676e737..7ef9505d8c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs @@ -117,7 +117,7 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.TransferHost(2); }); - AddStep("start with countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Delay = TimeSpan.FromMinutes(2) }).WaitSafely()); + AddStep("start with countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(2) }).WaitSafely()); ClickButtonWhenEnabled(); AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); @@ -193,7 +193,7 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.TransferHost(2); }); - AddStep("start countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Delay = TimeSpan.FromMinutes(1) }).WaitSafely()); + AddStep("start countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(1) }).WaitSafely()); AddUntilStep("countdown started", () => MultiplayerClient.Room?.Countdown != null); AddStep("transfer host to local user", () => MultiplayerClient.TransferHost(API.LocalUser.Value.OnlineID)); diff --git a/osu.Game/Online/Multiplayer/Countdown/StartMatchCountdownRequest.cs b/osu.Game/Online/Multiplayer/Countdown/StartMatchCountdownRequest.cs index 9e6967af9d..08eab26090 100644 --- a/osu.Game/Online/Multiplayer/Countdown/StartMatchCountdownRequest.cs +++ b/osu.Game/Online/Multiplayer/Countdown/StartMatchCountdownRequest.cs @@ -18,6 +18,6 @@ namespace osu.Game.Online.Multiplayer.Countdown /// How long the countdown should last. /// [Key(0)] - public TimeSpan Delay { get; set; } + public TimeSpan Duration { get; set; } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs index d97ac601d5..9470fb1d68 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs @@ -150,7 +150,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match Debug.Assert(clickOperation == null); clickOperation = ongoingOperationTracker.BeginOperation(); - Client.SendMatchRequest(new StartMatchCountdownRequest { Delay = duration }).ContinueWith(_ => endOperation()); + Client.SendMatchRequest(new StartMatchCountdownRequest { Duration = duration }).ContinueWith(_ => endOperation()); } private void endOperation() diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 938147e05e..1f20292437 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -315,7 +315,7 @@ namespace osu.Game.Tests.Visual.Multiplayer var stopSource = countdownStopSource = new CancellationTokenSource(); var finishSource = countdownFinishSource = new CancellationTokenSource(); var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(stopSource.Token, finishSource.Token); - var countdown = new MatchStartCountdown { TimeRemaining = matchCountdownRequest.Delay }; + var countdown = new MatchStartCountdown { TimeRemaining = matchCountdownRequest.Duration }; Task lastCountdownTask = countdownTask; countdownTask = start(); @@ -335,7 +335,7 @@ namespace osu.Game.Tests.Visual.Multiplayer try { - await Task.Delay(matchCountdownRequest.Delay, cancellationSource.Token).ConfigureAwait(false); + await Task.Delay(matchCountdownRequest.Duration, cancellationSource.Token).ConfigureAwait(false); } catch (OperationCanceledException) { From 2ea9e5245c381061e20d7b64ec86ef0795778287 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Mar 2022 20:15:13 +0900 Subject: [PATCH 14/43] Revert "Remove `IsLayered` from `GetHasCode` implementation" This reverts commit 16ee6b5fc7700b2a46359fb6b814e9c56eafaa53. --- osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 8dc037c7c8..47fe9032f0 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -505,7 +505,7 @@ namespace osu.Game.Rulesets.Objects.Legacy public override bool Equals(object? obj) => obj is LegacyHitSampleInfo other && Equals(other); - public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), CustomSampleBank); + public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), CustomSampleBank, IsLayered); } private class FileHitSampleInfo : LegacyHitSampleInfo, IEquatable From 997c091a8de3a3aaaa99a2cf2194da2117f5af3b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Mar 2022 20:15:17 +0900 Subject: [PATCH 15/43] Revert "Remove `IsLayered` from `LegacyHitSampleInfo` comparison" This reverts commit 45233932089ea26d9e329c47f1893137d2d4bcaf. --- osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 47fe9032f0..b091803406 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -500,7 +500,7 @@ namespace osu.Game.Rulesets.Objects.Legacy => new LegacyHitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newVolume.GetOr(Volume), newCustomSampleBank.GetOr(CustomSampleBank), newIsLayered.GetOr(IsLayered)); public bool Equals(LegacyHitSampleInfo? other) - => base.Equals(other) && CustomSampleBank == other.CustomSampleBank; + => base.Equals(other) && CustomSampleBank == other.CustomSampleBank && IsLayered == other.IsLayered; public override bool Equals(object? obj) => obj is LegacyHitSampleInfo other && Equals(other); From c079a9cd3232223379c793f284b63432bbe8eb93 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Mar 2022 20:18:44 +0900 Subject: [PATCH 16/43] Add comment regarding equality check importance in `LegacyHitSampleInfo` --- osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index b091803406..2a7f2b037f 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -500,6 +500,9 @@ namespace osu.Game.Rulesets.Objects.Legacy => new LegacyHitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newVolume.GetOr(Volume), newCustomSampleBank.GetOr(CustomSampleBank), newIsLayered.GetOr(IsLayered)); public bool Equals(LegacyHitSampleInfo? other) + // The additions to equality checks here are *required* to ensure that pooling works correctly. + // Of note, `IsLayered` may cause the usage of `SampleVirtual` instead of an actual sample (in cases playback is not required). + // Removing it would cause samples which may actually require playback to potentially source for a `SampleVirtual` sample pool. => base.Equals(other) && CustomSampleBank == other.CustomSampleBank && IsLayered == other.IsLayered; public override bool Equals(object? obj) From 547418e47e2296e71fb6567441afb183af2152d0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 24 Mar 2022 11:15:51 +0900 Subject: [PATCH 17/43] Revert "Remove PopoverButton class" This reverts commit 6b712be97d08de140bd6d3b0cb81d8d90412194b. --- .../Visual/Multiplayer/TestSceneMatchStartControl.cs | 8 ++++---- .../OnlinePlay/Multiplayer/Match/CountdownButton.cs | 6 +++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs index cd1eee49ec..b09fd306b7 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs @@ -79,7 +79,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { - var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); + var popoverButton = this.ChildrenOfType().First(); InputManager.MoveMouseTo(popoverButton); InputManager.Click(MouseButton.Left); }); @@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { - var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); + var popoverButton = this.ChildrenOfType().First(); InputManager.MoveMouseTo(popoverButton); InputManager.Click(MouseButton.Left); }); @@ -150,7 +150,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { - var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); + var popoverButton = this.ChildrenOfType().First(); InputManager.MoveMouseTo(popoverButton); InputManager.Click(MouseButton.Left); }); @@ -173,7 +173,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { - var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); + var popoverButton = this.ChildrenOfType().First(); InputManager.MoveMouseTo(popoverButton); InputManager.Click(MouseButton.Left); }); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs index e37168bf25..e598f6670c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs @@ -64,7 +64,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match foreach (var duration in available_delays) { - flow.Add(new OsuButton + flow.Add(new PopoverButton { RelativeSizeAxes = Axes.X, Text = $"Start match in {duration.Humanize()}", @@ -79,5 +79,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match return new OsuPopover { Child = flow }; } + + public class PopoverButton : OsuButton + { + } } } From a4d17a915f96c505e518dd261f9b4d1d5d967b7f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 12:36:16 +0900 Subject: [PATCH 18/43] Fix incorrect HUD component fallback Legacy skins should now always show the legacy hud components. The conditional here is no longer valid as fallback lookups happen at a *skin*-fallback level rather than internal *source*-fallback. Put another way, `LegacyDefaultSkin` (with user customisations) should still display the classic HUD components even if a font is not provided, as that font will be available via the skin lookup hierarchy. The TODO removed in this commit has been already resolved so this code is no longer required. --- osu.Game/Skinning/LegacySkin.cs | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 1c2ca797c6..244774fd4c 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -356,26 +356,15 @@ namespace osu.Game.Skinning } }) { - Children = this.HasFont(LegacyFont.Score) - ? new Drawable[] - { - new LegacyComboCounter(), - new LegacyScoreCounter(), - new LegacyAccuracyCounter(), - new LegacyHealthDisplay(), - new SongProgress(), - new BarHitErrorMeter(), - } - : new Drawable[] - { - // TODO: these should fallback to using osu!classic skin textures, rather than doing this. - new DefaultComboCounter(), - new DefaultScoreCounter(), - new DefaultAccuracyCounter(), - new DefaultHealthDisplay(), - new SongProgress(), - new BarHitErrorMeter(), - } + Children = new Drawable[] + { + new LegacyComboCounter(), + new LegacyScoreCounter(), + new LegacyAccuracyCounter(), + new LegacyHealthDisplay(), + new SongProgress(), + new BarHitErrorMeter(), + } }; return skinnableTargetWrapper; From 0cd29a73b929b007c02619d96fa6b71f0834aa24 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 12:39:47 +0900 Subject: [PATCH 19/43] Fix typo in xmldocs --- osu.Game/Skinning/Skin.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index b6f46a0d81..e1fcf196f3 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -25,13 +25,13 @@ namespace osu.Game.Skinning public abstract class Skin : IDisposable, ISkin { /// - /// A texture store which can be used to perform user file loops for this skin. + /// A texture store which can be used to perform user file lookups for this skin. /// [CanBeNull] protected TextureStore Textures { get; } /// - /// A sample store which can be used to perform user file loops for this skin. + /// A sample store which can be used to perform user file lookups for this skin. /// [CanBeNull] protected ISampleStore Samples { get; } From e243a7c55d349788f85c34a1b573b88547ace35b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 12:45:11 +0900 Subject: [PATCH 20/43] Reword `storage` param xmldoc to use stronger and better defined langauge --- osu.Game/Skinning/Skin.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index e1fcf196f3..e00dd950a7 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -57,7 +57,7 @@ namespace osu.Game.Skinning /// /// The skin's metadata. Usually a live realm object. /// Access to game-wide resources. - /// An optional store which will be used for looking up skin resources. If null, one will be created from realm pattern. + /// An optional store which will *replace* all file lookups that are usually sourced from . /// An optional filename to read the skin configuration from. If not provided, the configuration will be retrieved from the storage using "skin.ini". protected Skin(SkinInfo skin, [CanBeNull] IStorageResourceProvider resources, [CanBeNull] IResourceStore storage = null, [CanBeNull] string configurationFilename = @"skin.ini") { From 4d0b4c2541823d083c235c6b390a8d17d2f73ac9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 12:53:50 +0900 Subject: [PATCH 21/43] Fix realm potentially not being refreshed in time for test asserts in `BeatmapImporterTests` As seen at https://github.com/ppy/osu/runs/5659368512?check_suite_focus=true Went through every usage of `.Import` and added either an `EnsureLoaded`, or where that provides too many checks, an explicit `realm.Refresh()`. --- osu.Game.Tests/Database/BeatmapImporterTests.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index 9abd78039a..f9c13a8169 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -147,7 +147,10 @@ namespace osu.Game.Tests.Database Live? imported; using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream())) + { imported = await importer.Import(reader); + EnsureLoaded(realm.Realm); + } Assert.AreEqual(1, realm.Realm.All().Count()); @@ -510,6 +513,8 @@ namespace osu.Game.Tests.Database new ImportTask(zipStream, string.Empty) ); + realm.Run(r => r.Refresh()); + checkBeatmapSetCount(realm.Realm, 0); checkBeatmapCount(realm.Realm, 0); @@ -565,6 +570,8 @@ namespace osu.Game.Tests.Database { } + EnsureLoaded(realm.Realm); + checkBeatmapSetCount(realm.Realm, 1); checkBeatmapCount(realm.Realm, 12); @@ -726,6 +733,8 @@ namespace osu.Game.Tests.Database var imported = importer.Import(toImport); + realm.Run(r => r.Refresh()); + Assert.NotNull(imported); Debug.Assert(imported != null); @@ -891,6 +900,8 @@ namespace osu.Game.Tests.Database string? temp = TestResources.GetTestBeatmapForImport(); await importer.Import(temp); + EnsureLoaded(realm.Realm); + // Update via the beatmap, not the beatmap info, to ensure correct linking BeatmapSetInfo setToUpdate = realm.Realm.All().First(); From 90c7945bca3c413824618dc3ae430d072e2d7a85 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 24 Mar 2022 14:26:31 +0900 Subject: [PATCH 22/43] Re-remove PopoverButton class with better test fix --- .../Visual/Multiplayer/TestSceneMatchStartControl.cs | 9 +++++---- .../OnlinePlay/Multiplayer/Match/CountdownButton.cs | 6 +----- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs index b09fd306b7..02a61208d4 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs @@ -10,6 +10,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Framework.Utils; @@ -79,7 +80,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { - var popoverButton = this.ChildrenOfType().First(); + var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); InputManager.MoveMouseTo(popoverButton); InputManager.Click(MouseButton.Left); }); @@ -97,7 +98,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { - var popoverButton = this.ChildrenOfType().First(); + var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); InputManager.MoveMouseTo(popoverButton); InputManager.Click(MouseButton.Left); }); @@ -150,7 +151,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { - var popoverButton = this.ChildrenOfType().First(); + var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); InputManager.MoveMouseTo(popoverButton); InputManager.Click(MouseButton.Left); }); @@ -173,7 +174,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { - var popoverButton = this.ChildrenOfType().First(); + var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); InputManager.MoveMouseTo(popoverButton); InputManager.Click(MouseButton.Left); }); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs index e598f6670c..e37168bf25 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs @@ -64,7 +64,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match foreach (var duration in available_delays) { - flow.Add(new PopoverButton + flow.Add(new OsuButton { RelativeSizeAxes = Axes.X, Text = $"Start match in {duration.Humanize()}", @@ -79,9 +79,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match return new OsuPopover { Child = flow }; } - - public class PopoverButton : OsuButton - { - } } } From 96a447f68bf753b9c9f914def65504c627c9a27c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 24 Mar 2022 14:28:38 +0900 Subject: [PATCH 23/43] Rename Multiplayer prefix to button classes --- .../Visual/Multiplayer/QueueModeTestScene.cs | 4 +- .../TestSceneAllPlayersQueueMode.cs | 8 +-- .../Multiplayer/TestSceneMatchStartControl.cs | 60 +++++++++---------- .../TestSceneMultiplayerMatchSubScreen.cs | 2 +- .../TestSceneMultiplayerSpectateButton.cs | 2 +- .../Multiplayer/Match/MatchStartControl.cs | 6 +- ...utton.cs => MultiplayerCountdownButton.cs} | 4 +- ...adyButton.cs => MultiplayerReadyButton.cs} | 3 +- 8 files changed, 45 insertions(+), 44 deletions(-) rename osu.Game/Screens/OnlinePlay/Multiplayer/Match/{CountdownButton.cs => MultiplayerCountdownButton.cs} (95%) rename osu.Game/Screens/OnlinePlay/Multiplayer/Match/{ReadyButton.cs => MultiplayerReadyButton.cs} (98%) diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index fd43674b3b..bafc579134 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -95,10 +95,10 @@ namespace osu.Game.Tests.Visual.Multiplayer protected void RunGameplay() { AddUntilStep("wait for idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for player", () => multiplayerComponents.CurrentScreen is Player player && player.IsLoaded); AddStep("exit player", () => multiplayerComponents.MultiplayerScreen.MakeCurrent()); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs index 582dacb332..0785315b26 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs @@ -102,10 +102,10 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID); AddUntilStep("wait for idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for player", () => CurrentScreen is Player player && player.IsLoaded); AddAssert("ruleset is correct", () => ((Player)CurrentScreen).Ruleset.Value.Equals(new OsuRuleset().RulesetInfo)); @@ -119,10 +119,10 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID); AddUntilStep("wait for idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for player", () => CurrentScreen is Player player && player.IsLoaded); AddAssert("mods are correct", () => !((Player)CurrentScreen).Mods.Value.Any()); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs index 02a61208d4..4fd6fd5d70 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs @@ -75,9 +75,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestStartWithCountdown() { - ClickButtonWhenEnabled(); - AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); + AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); @@ -85,7 +85,7 @@ namespace osu.Game.Tests.Visual.Multiplayer InputManager.Click(MouseButton.Left); }); - AddAssert("countdown button not visible", () => !this.ChildrenOfType().Single().IsPresent); + AddAssert("countdown button not visible", () => !this.ChildrenOfType().Single().IsPresent); AddStep("finish countdown", () => MultiplayerClient.FinishCountdown()); AddUntilStep("match started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.WaitingForLoad); } @@ -93,9 +93,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestCancelCountdown() { - ClickButtonWhenEnabled(); - AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); + AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); @@ -103,7 +103,7 @@ namespace osu.Game.Tests.Visual.Multiplayer InputManager.Click(MouseButton.Left); }); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddStep("finish countdown", () => MultiplayerClient.FinishCountdown()); AddUntilStep("match not started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); @@ -120,10 +120,10 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("start with countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(2) }).WaitSafely()); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle); } @@ -133,22 +133,22 @@ namespace osu.Game.Tests.Visual.Multiplayer 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().Single().IsPresent); - AddAssert("countdown button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); + AddAssert("countdown button is visible", () => this.ChildrenOfType().Single().IsPresent); + AddAssert("countdown button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); AddStep("add second user", () => MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" })); - AddAssert("countdown button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); + AddAssert("countdown button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); AddStep("set second user ready", () => MultiplayerClient.ChangeUserState(2, MultiplayerUserState.Ready)); - AddAssert("countdown button enabled", () => this.ChildrenOfType().Single().Enabled.Value); + AddAssert("countdown button enabled", () => this.ChildrenOfType().Single().Enabled.Value); } [Test] public void TestSpectatingDuringCountdownWithNoReadyUsersCancelsCountdown() { - ClickButtonWhenEnabled(); - AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); + AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); @@ -169,9 +169,9 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("add second user", () => MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" })); AddStep("set second user ready", () => MultiplayerClient.ChangeUserState(2, MultiplayerUserState.Ready)); - ClickButtonWhenEnabled(); - AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); + AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); @@ -182,7 +182,7 @@ namespace osu.Game.Tests.Visual.Multiplayer 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().Single().Enabled.Value); + AddAssert("ready button enabled", () => this.ChildrenOfType().Single().Enabled.Value); } [Test] @@ -200,7 +200,7 @@ namespace osu.Game.Tests.Visual.Multiplayer 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(); + ClickButtonWhenEnabled(); AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); AddAssert("countdown still active", () => MultiplayerClient.Room?.Countdown != null); } @@ -231,10 +231,10 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.TransferHost(2); }); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle); } @@ -250,7 +250,7 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }); }); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); verifyGameplayStartFlow(); @@ -265,7 +265,7 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.TransferHost(2); }); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddStep("make user host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0)); verifyGameplayStartFlow(); @@ -280,12 +280,12 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }); }); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); AddStep("transfer host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[1].UserID ?? 0)); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is idle (match not started)", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle); AddUntilStep("ready button enabled", () => control.ChildrenOfType().Single().Enabled.Value); } @@ -305,7 +305,7 @@ namespace osu.Game.Tests.Visual.Multiplayer if (!isHost) AddStep("transfer host", () => MultiplayerClient.TransferHost(2)); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddRepeatStep("change user ready state", () => { @@ -323,7 +323,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private void verifyGameplayStartFlow() { AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user waiting for load", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad); AddStep("finish gameplay", () => diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 850a115f4c..057032c413 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -145,7 +145,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for spectating user state", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("match started", () => MultiplayerClient.Room?.State == MultiplayerRoomState.WaitingForLoad); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index 07ac580276..13917f4eb0 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -146,6 +146,6 @@ namespace osu.Game.Tests.Visual.Multiplayer => AddUntilStep($"spectate button {(shouldBeEnabled ? "is" : "is not")} enabled", () => spectateButton.ChildrenOfType().Single().Enabled.Value == shouldBeEnabled); private void assertReadyButtonEnablement(bool shouldBeEnabled) - => AddUntilStep($"ready button {(shouldBeEnabled ? "is" : "is not")} enabled", () => startControl.ChildrenOfType().Single().Enabled.Value == shouldBeEnabled); + => AddUntilStep($"ready button {(shouldBeEnabled ? "is" : "is not")} enabled", () => startControl.ChildrenOfType().Single().Enabled.Value == shouldBeEnabled); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs index 9470fb1d68..af7ed9b9e2 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs @@ -31,7 +31,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private Sample sampleUnready; private readonly BindableBool enabled = new BindableBool(); - private readonly CountdownButton countdownButton; + private readonly MultiplayerCountdownButton countdownButton; private int countReady; private ScheduledDelegate readySampleDelegate; private IBindable operationInProgress; @@ -50,14 +50,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { new Drawable[] { - new ReadyButton + new MultiplayerReadyButton { RelativeSizeAxes = Axes.Both, Size = Vector2.One, Action = onReadyClick, Enabled = { BindTarget = enabled }, }, - countdownButton = new CountdownButton + countdownButton = new MultiplayerCountdownButton { RelativeSizeAxes = Axes.Y, Size = new Vector2(40, 1), diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs similarity index 95% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs index e37168bf25..3bf7e91a55 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs @@ -18,7 +18,7 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { - public class CountdownButton : IconButton, IHasPopover + public class MultiplayerCountdownButton : IconButton, IHasPopover { private static readonly TimeSpan[] available_delays = { @@ -32,7 +32,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private readonly Drawable background; - public CountdownButton() + public MultiplayerCountdownButton() { Icon = FontAwesome.Solid.CaretDown; IconScale = new Vector2(0.6f); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs similarity index 98% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Match/ReadyButton.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 007e055d8c..6ff717d5c3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -10,10 +10,11 @@ using osu.Framework.Threading; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Online.Multiplayer; +using osu.Game.Screens.OnlinePlay.Components; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { - public class ReadyButton : Components.ReadyButton + public class MultiplayerReadyButton : ReadyButton { public new Triangles Triangles => base.Triangles; From d36944ac950d86761931befc4a7e020040d9ce1e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 24 Mar 2022 14:35:03 +0900 Subject: [PATCH 24/43] Dispose token manually Cover more branches with cancellation source disposal --- .../Visual/Multiplayer/TestMultiplayerClient.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 1f20292437..5fa2ca8890 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -343,19 +343,19 @@ namespace osu.Game.Tests.Visual.Multiplayer Schedule(() => { - if (Room.Countdown != countdown) - return; - - Room.Countdown = null; - MatchEvent(new CountdownChangedEvent { Countdown = null }); - using (cancellationSource) { + if (Room.Countdown != countdown) + return; + + Room.Countdown = null; + MatchEvent(new CountdownChangedEvent { Countdown = null }); + if (stopSource.Token.IsCancellationRequested) return; - } - StartMatch().WaitSafely(); + StartMatch().WaitSafely(); + } }); } From 8f3a4df70adc38f9ed4fb89e31b3367012eb8e45 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 24 Mar 2022 14:44:27 +0900 Subject: [PATCH 25/43] Add explanation for try-catch --- osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 5fa2ca8890..f96ba8ba24 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -339,6 +339,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } catch (OperationCanceledException) { + // Clients need to be notified of cancellations in the following code. } Schedule(() => From d2ecc100e5390460a4216ac5e4ccc54ce62f89aa Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 24 Mar 2022 15:07:01 +0900 Subject: [PATCH 26/43] Revert unnecessary async change --- osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index f96ba8ba24..7562b1d0e2 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -387,7 +387,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } } - public override async Task StartMatch() + public override Task StartMatch() { Debug.Assert(Room != null); @@ -395,7 +395,7 @@ namespace osu.Game.Tests.Visual.Multiplayer foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready)) ChangeUserState(user.UserID, MultiplayerUserState.WaitingForLoad); - await ((IMultiplayerClient)this).LoadRequested(); + return ((IMultiplayerClient)this).LoadRequested(); } public override Task AbortGameplay() From f0d132b16e04e85bb84c8b13d42d15559d2ce4e5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 24 Mar 2022 15:21:46 +0900 Subject: [PATCH 27/43] Rename FinishCountdown() -> SkipToEndOfCountdown() --- .../Visual/Multiplayer/TestSceneMatchStartControl.cs | 6 +++--- .../Visual/Multiplayer/TestMultiplayerClient.cs | 12 ++++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs index 4fd6fd5d70..a374488306 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs @@ -86,7 +86,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddAssert("countdown button not visible", () => !this.ChildrenOfType().Single().IsPresent); - AddStep("finish countdown", () => MultiplayerClient.FinishCountdown()); + AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown()); AddUntilStep("match started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.WaitingForLoad); } @@ -105,7 +105,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); - AddStep("finish countdown", () => MultiplayerClient.FinishCountdown()); + AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown()); AddUntilStep("match not started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); } @@ -159,7 +159,7 @@ namespace osu.Game.Tests.Visual.Multiplayer 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()); + AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown()); AddUntilStep("match not started", () => MultiplayerClient.Room?.State == MultiplayerRoomState.Open); } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 7562b1d0e2..ea991af914 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -296,11 +296,15 @@ namespace osu.Game.Tests.Visual.Multiplayer return Task.CompletedTask; } - private CancellationTokenSource? countdownFinishSource; + private CancellationTokenSource? countdownSkipSource; private CancellationTokenSource? countdownStopSource; private Task countdownTask = Task.CompletedTask; - public void FinishCountdown() => countdownFinishSource?.Cancel(); + /// + /// Skips to the end of the currently-running countdown, if one is running, + /// and runs the callback (e.g. to start the match) as soon as possible unless the countdown has been cancelled. + /// + public void SkipToEndOfCountdown() => countdownSkipSource?.Cancel(); public override async Task SendMatchRequest(MatchUserRequest request) { @@ -313,8 +317,8 @@ namespace osu.Game.Tests.Visual.Multiplayer countdownStopSource?.Cancel(); var stopSource = countdownStopSource = new CancellationTokenSource(); - var finishSource = countdownFinishSource = new CancellationTokenSource(); - var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(stopSource.Token, finishSource.Token); + var skipSource = countdownSkipSource = new CancellationTokenSource(); + var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(stopSource.Token, skipSource.Token); var countdown = new MatchStartCountdown { TimeRemaining = matchCountdownRequest.Duration }; Task lastCountdownTask = countdownTask; From 4c0d76573c10a0ae54463d491367ce43c0e3a691 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 24 Mar 2022 15:51:30 +0900 Subject: [PATCH 28/43] Asserate code is running on update thread --- osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index ea991af914..6f57d818a4 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -11,6 +11,7 @@ using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Development; using osu.Framework.Extensions; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; @@ -314,6 +315,8 @@ namespace osu.Game.Tests.Visual.Multiplayer switch (request) { case StartMatchCountdownRequest matchCountdownRequest: + Debug.Assert(ThreadSafety.IsUpdateThread); + countdownStopSource?.Cancel(); var stopSource = countdownStopSource = new CancellationTokenSource(); From a7d5f2281ce59c004991070865a6c4cf640bc637 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 15:49:13 +0900 Subject: [PATCH 29/43] Apply beatmap offsets to legacy replay frame handling --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 8 +++++++- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 6 +++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index fefee370b9..9885fe5528 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -23,6 +23,8 @@ namespace osu.Game.Scoring.Legacy private IBeatmap currentBeatmap; private Ruleset currentRuleset; + private float beatmapOffset; + public Score Parse(Stream stream) { var score = new Score @@ -72,6 +74,10 @@ namespace osu.Game.Scoring.Legacy currentBeatmap = workingBeatmap.GetPlayableBeatmap(currentRuleset.RulesetInfo, scoreInfo.Mods); scoreInfo.BeatmapInfo = currentBeatmap.BeatmapInfo; + // BeatmapVersion 4 and lower had an incorrect offset (stable has this set as 24ms off) + // As this is baked into hitobject timing (see `LegacyBeatmapDecoder`) we also need to apply this to replay frame timing. + beatmapOffset = currentBeatmap.BeatmapInfo.BeatmapVersion < 5 ? 24 : 0; + /* score.HpGraphString = */ sr.ReadString(); @@ -229,7 +235,7 @@ namespace osu.Game.Scoring.Legacy private void readLegacyReplay(Replay replay, StreamReader reader) { - float lastTime = 0; + float lastTime = beatmapOffset; ReplayFrame currentFrame = null; string[] frames = reader.ReadToEnd().Split(','); diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index f0ead05280..6a321bed7a 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -111,6 +111,10 @@ namespace osu.Game.Scoring.Legacy { StringBuilder replayData = new StringBuilder(); + // BeatmapVersion 4 and lower had an incorrect offset (stable has this set as 24ms off) + // As this is baked into hitobject timing (see `LegacyBeatmapDecoder`) we also need to apply this to replay frame timing. + double offset = beatmap?.BeatmapInfo.BeatmapVersion < 5 ? -24 : 0; + if (score.Replay != null) { int lastTime = 0; @@ -120,7 +124,7 @@ namespace osu.Game.Scoring.Legacy var legacyFrame = getLegacyFrame(f); // Rounding because stable could only parse integral values - int time = (int)Math.Round(legacyFrame.Time); + int time = (int)Math.Round(legacyFrame.Time + offset); replayData.Append(FormattableString.Invariant($"{time - lastTime}|{legacyFrame.MouseX ?? 0}|{legacyFrame.MouseY ?? 0}|{(int)legacyFrame.ButtonState},")); lastTime = time; } From dfa076c16983fe5badebd5bc3887cdf81d2fe2fb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 24 Mar 2022 16:29:59 +0900 Subject: [PATCH 30/43] Refactor cancellation logic --- .../Multiplayer/TestMultiplayerClient.cs | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 6f57d818a4..9be1b18062 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -319,9 +319,10 @@ namespace osu.Game.Tests.Visual.Multiplayer countdownStopSource?.Cancel(); + // Note that this will leak CTSs, however this is a test method and we haven't noticed foregoing disposal of non-linked CTSs to be detrimental. + // If necessary, this can be moved into the final schedule below, and the class-level fields be nulled out accordingly. var stopSource = countdownStopSource = new CancellationTokenSource(); var skipSource = countdownSkipSource = new CancellationTokenSource(); - var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(stopSource.Token, skipSource.Token); var countdown = new MatchStartCountdown { TimeRemaining = matchCountdownRequest.Duration }; Task lastCountdownTask = countdownTask; @@ -342,7 +343,8 @@ namespace osu.Game.Tests.Visual.Multiplayer try { - await Task.Delay(matchCountdownRequest.Duration, cancellationSource.Token).ConfigureAwait(false); + using (var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(stopSource.Token, skipSource.Token)) + await Task.Delay(matchCountdownRequest.Duration, cancellationSource.Token).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -351,19 +353,16 @@ namespace osu.Game.Tests.Visual.Multiplayer Schedule(() => { - using (cancellationSource) - { - if (Room.Countdown != countdown) - return; + if (Room.Countdown != countdown) + return; - Room.Countdown = null; - MatchEvent(new CountdownChangedEvent { Countdown = null }); + Room.Countdown = null; + MatchEvent(new CountdownChangedEvent { Countdown = null }); - if (stopSource.Token.IsCancellationRequested) - return; + if (stopSource.IsCancellationRequested) + return; - StartMatch().WaitSafely(); - } + StartMatch().WaitSafely(); }); } From 59a7eb532203629e8dbd6dbc8f44cbc83c03fbde Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 16:34:21 +0900 Subject: [PATCH 31/43] Add test coverage ensuring offsets are correct before and after legacy replay encode --- .../Formats/LegacyScoreDecoderTest.cs | 60 ++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index 2ba8c51a10..a50cef238a 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -8,6 +8,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Formats; using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; @@ -64,6 +65,55 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [TestCase(3)] + [TestCase(6)] + [TestCase(LegacyBeatmapDecoder.LATEST_VERSION)] + public void TestLegacyBeatmapReplayOffsets(int beatmapVersion) + { + const double first_frame_time = 2000; + const double second_frame_time = 3000; + + var ruleset = new OsuRuleset().RulesetInfo; + var scoreInfo = TestResources.CreateTestScoreInfo(ruleset); + var beatmap = new TestBeatmap(ruleset) + { + BeatmapInfo = + { + BeatmapVersion = beatmapVersion + } + }; + + var score = new Score + { + ScoreInfo = scoreInfo, + Replay = new Replay + { + Frames = new List + { + new OsuReplayFrame(first_frame_time, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton), + new OsuReplayFrame(second_frame_time, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton) + } + } + }; + + // the "se" culture is used here, as it encodes the negative number sign as U+2212 MINUS SIGN, + // rather than the classic ASCII U+002D HYPHEN-MINUS. + CultureInfo.CurrentCulture = new CultureInfo("se"); + + var encodeStream = new MemoryStream(); + + var encoder = new LegacyScoreEncoder(score, beatmap); + encoder.Encode(encodeStream); + + var decodeStream = new MemoryStream(encodeStream.GetBuffer()); + + var decoder = new TestLegacyScoreDecoder(beatmapVersion); + var decodedAfterEncode = decoder.Parse(decodeStream); + + Assert.That(decodedAfterEncode.Replay.Frames[0].Time, Is.EqualTo(first_frame_time)); + Assert.That(decodedAfterEncode.Replay.Frames[1].Time, Is.EqualTo(second_frame_time)); + } + [Test] public void TestCultureInvariance() { @@ -118,6 +168,8 @@ namespace osu.Game.Tests.Beatmaps.Formats private class TestLegacyScoreDecoder : LegacyScoreDecoder { + private readonly int beatmapVersion; + private static readonly Dictionary rulesets = new Ruleset[] { new OsuRuleset(), @@ -126,6 +178,11 @@ namespace osu.Game.Tests.Beatmaps.Formats new ManiaRuleset() }.ToDictionary(ruleset => ((ILegacyRuleset)ruleset).LegacyID); + public TestLegacyScoreDecoder(int beatmapVersion = LegacyBeatmapDecoder.LATEST_VERSION) + { + this.beatmapVersion = beatmapVersion; + } + protected override Ruleset GetRuleset(int rulesetId) => rulesets[rulesetId]; protected override WorkingBeatmap GetBeatmap(string md5Hash) => new TestWorkingBeatmap(new Beatmap @@ -134,7 +191,8 @@ namespace osu.Game.Tests.Beatmaps.Formats { MD5Hash = md5Hash, Ruleset = new OsuRuleset().RulesetInfo, - Difficulty = new BeatmapDifficulty() + Difficulty = new BeatmapDifficulty(), + BeatmapVersion = beatmapVersion, } }); } From 2efae031c97a904c7876f6ee89093725b70f1bc5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 16:39:56 +0900 Subject: [PATCH 32/43] Add test coverage of decode specifically --- .../Formats/LegacyScoreDecoderTest.cs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index a50cef238a..39586bcd8c 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -65,10 +65,29 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [TestCase(3, true)] + [TestCase(6, false)] + [TestCase(LegacyBeatmapDecoder.LATEST_VERSION, false)] + public void TestLegacyBeatmapReplayOffsetsDecode(int beatmapVersion, bool offsetApplied) + { + const double first_frame_time = 48; + const double second_frame_time = 65; + + var decoder = new TestLegacyScoreDecoder(beatmapVersion); + + using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr")) + { + var score = decoder.Parse(resourceStream); + + Assert.That(score.Replay.Frames[0].Time, Is.EqualTo(first_frame_time + (offsetApplied ? 24 : 0))); + Assert.That(score.Replay.Frames[1].Time, Is.EqualTo(second_frame_time + (offsetApplied ? 24 : 0))); + } + } + [TestCase(3)] [TestCase(6)] [TestCase(LegacyBeatmapDecoder.LATEST_VERSION)] - public void TestLegacyBeatmapReplayOffsets(int beatmapVersion) + public void TestLegacyBeatmapReplayOffsetsEncodeDecode(int beatmapVersion) { const double first_frame_time = 2000; const double second_frame_time = 3000; From a7554dcdf77637543960f712cbbcb521fc3f410b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 16:43:41 +0900 Subject: [PATCH 33/43] Use a constant for the early version timing offset --- osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs | 4 ++-- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 8 ++++++-- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 3 +-- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 8 ++++---- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index 39586bcd8c..1cd910789f 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -79,8 +79,8 @@ namespace osu.Game.Tests.Beatmaps.Formats { var score = decoder.Parse(resourceStream); - Assert.That(score.Replay.Frames[0].Time, Is.EqualTo(first_frame_time + (offsetApplied ? 24 : 0))); - Assert.That(score.Replay.Frames[1].Time, Is.EqualTo(second_frame_time + (offsetApplied ? 24 : 0))); + Assert.That(score.Replay.Frames[0].Time, Is.EqualTo(first_frame_time + (offsetApplied ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0))); + Assert.That(score.Replay.Frames[1].Time, Is.EqualTo(second_frame_time + (offsetApplied ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0))); } } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index e2a043490f..79d8bd3bb3 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -19,6 +19,11 @@ namespace osu.Game.Beatmaps.Formats { public class LegacyBeatmapDecoder : LegacyDecoder { + /// + /// An offset which needs to be applied to old beatmaps (v4 and lower) to correct timing changes that were applied at a game client level. + /// + public const int EARLY_VERSION_TIMING_OFFSET = 24; + internal static RulesetStore RulesetStore; private Beatmap beatmap; @@ -50,8 +55,7 @@ namespace osu.Game.Beatmaps.Formats RulesetStore = new AssemblyRulesetStore(); } - // BeatmapVersion 4 and lower had an incorrect offset (stable has this set as 24ms off) - offset = FormatVersion < 5 ? 24 : 0; + offset = FormatVersion < 5 ? EARLY_VERSION_TIMING_OFFSET : 0; } protected override Beatmap CreateTemplateObject() diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 9885fe5528..754ace82c5 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -74,9 +74,8 @@ namespace osu.Game.Scoring.Legacy currentBeatmap = workingBeatmap.GetPlayableBeatmap(currentRuleset.RulesetInfo, scoreInfo.Mods); scoreInfo.BeatmapInfo = currentBeatmap.BeatmapInfo; - // BeatmapVersion 4 and lower had an incorrect offset (stable has this set as 24ms off) // As this is baked into hitobject timing (see `LegacyBeatmapDecoder`) we also need to apply this to replay frame timing. - beatmapOffset = currentBeatmap.BeatmapInfo.BeatmapVersion < 5 ? 24 : 0; + beatmapOffset = currentBeatmap.BeatmapInfo.BeatmapVersion < 5 ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0; /* score.HpGraphString = */ sr.ReadString(); diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 6a321bed7a..ae9afbf32e 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -1,12 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System; using System.IO; using System.Linq; using System.Text; using osu.Framework.Extensions; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Formats; using osu.Game.Extensions; using osu.Game.IO.Legacy; using osu.Game.Replays.Legacy; @@ -14,8 +17,6 @@ using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; using SharpCompress.Compressors.LZMA; -#nullable enable - namespace osu.Game.Scoring.Legacy { public class LegacyScoreEncoder @@ -111,9 +112,8 @@ namespace osu.Game.Scoring.Legacy { StringBuilder replayData = new StringBuilder(); - // BeatmapVersion 4 and lower had an incorrect offset (stable has this set as 24ms off) // As this is baked into hitobject timing (see `LegacyBeatmapDecoder`) we also need to apply this to replay frame timing. - double offset = beatmap?.BeatmapInfo.BeatmapVersion < 5 ? -24 : 0; + double offset = beatmap?.BeatmapInfo.BeatmapVersion < 5 ? -LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0; if (score.Replay != null) { From d6fc53579eb0d7d5d3eed86575006b8820d9135e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 17:00:49 +0900 Subject: [PATCH 34/43] Split out shared code for encode-decode cycle (and remove unrelated culture set) --- .../Formats/LegacyScoreDecoderTest.cs | 38 ++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index 1cd910789f..1474f2d277 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -115,19 +115,7 @@ namespace osu.Game.Tests.Beatmaps.Formats } }; - // the "se" culture is used here, as it encodes the negative number sign as U+2212 MINUS SIGN, - // rather than the classic ASCII U+002D HYPHEN-MINUS. - CultureInfo.CurrentCulture = new CultureInfo("se"); - - var encodeStream = new MemoryStream(); - - var encoder = new LegacyScoreEncoder(score, beatmap); - encoder.Encode(encodeStream); - - var decodeStream = new MemoryStream(encodeStream.GetBuffer()); - - var decoder = new TestLegacyScoreDecoder(beatmapVersion); - var decodedAfterEncode = decoder.Parse(decodeStream); + var decodedAfterEncode = encodeThenDecode(beatmapVersion, score, beatmap); Assert.That(decodedAfterEncode.Replay.Frames[0].Time, Is.EqualTo(first_frame_time)); Assert.That(decodedAfterEncode.Replay.Frames[1].Time, Is.EqualTo(second_frame_time)); @@ -155,15 +143,7 @@ namespace osu.Game.Tests.Beatmaps.Formats // rather than the classic ASCII U+002D HYPHEN-MINUS. CultureInfo.CurrentCulture = new CultureInfo("se"); - var encodeStream = new MemoryStream(); - - var encoder = new LegacyScoreEncoder(score, beatmap); - encoder.Encode(encodeStream); - - var decodeStream = new MemoryStream(encodeStream.GetBuffer()); - - var decoder = new TestLegacyScoreDecoder(); - var decodedAfterEncode = decoder.Parse(decodeStream); + var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap); Assert.Multiple(() => { @@ -179,6 +159,20 @@ namespace osu.Game.Tests.Beatmaps.Formats }); } + private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap) + { + var encodeStream = new MemoryStream(); + + var encoder = new LegacyScoreEncoder(score, beatmap); + encoder.Encode(encodeStream); + + var decodeStream = new MemoryStream(encodeStream.GetBuffer()); + + var decoder = new TestLegacyScoreDecoder(beatmapVersion); + var decodedAfterEncode = decoder.Parse(decodeStream); + return decodedAfterEncode; + } + [TearDown] public void TearDown() { From 528ffea38db1b368b49a5c203fb744d33cbb6568 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 24 Mar 2022 17:11:08 +0900 Subject: [PATCH 35/43] Fix incorrect event binding --- .../OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 6ff717d5c3..746e4257f1 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -31,7 +31,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { base.LoadComplete(); - multiplayerClient.RoomUpdated += () => Scheduler.AddOnce(onRoomUpdated); + multiplayerClient.RoomUpdated += onRoomUpdated; onRoomUpdated(); } @@ -39,7 +39,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private DateTimeOffset countdownReceivedTime; private ScheduledDelegate countdownUpdateDelegate; - private void onRoomUpdated() + private void onRoomUpdated() => Scheduler.AddOnce(() => { if (countdown == null && room?.Countdown != null) countdownReceivedTime = DateTimeOffset.Now; @@ -56,7 +56,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match updateButtonText(); updateButtonColour(); - } + }); private void updateButtonText() { From e3f8bc05883e126e176d6b12e54e0df39c85003a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 17:14:51 +0900 Subject: [PATCH 36/43] Revert `Availability` to `private` --- .../Screens/OnlinePlay/Components/ReadyButton.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs index 79cf5c7236..cdaa39d2be 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs @@ -14,18 +14,20 @@ namespace osu.Game.Screens.OnlinePlay.Components public abstract class ReadyButton : TriangleButton, IHasTooltip { public new readonly BindableBool Enabled = new BindableBool(); - protected readonly IBindable Availability = new Bindable(); + + private readonly IBindable availability = new Bindable(); [BackgroundDependencyLoader] private void load(OnlinePlayBeatmapAvailabilityTracker beatmapTracker) { - Availability.BindTo(beatmapTracker.Availability); - 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 { @@ -34,7 +36,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; From e889d9344159d6c38641d6eaae56c4ab957a34d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 17:47:58 +0900 Subject: [PATCH 37/43] Add asserts of playlist being non-empty after client operations --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 2d5496c5c1..faa995ed19 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -171,6 +171,8 @@ namespace osu.Game.Online.Multiplayer Room = joinedRoom; APIRoom = room; + Debug.Assert(joinedRoom.Playlist.Count > 0); + APIRoom.Playlist.Clear(); APIRoom.Playlist.AddRange(joinedRoom.Playlist.Select(createPlaylistItem)); @@ -683,6 +685,8 @@ namespace osu.Game.Online.Multiplayer Room.Playlist.Remove(Room.Playlist.Single(existing => existing.ID == playlistItemId)); APIRoom.Playlist.RemoveAll(existing => existing.ID == playlistItemId); + Debug.Assert(Room.Playlist.Count > 0); + ItemRemoved?.Invoke(playlistItemId); RoomUpdated?.Invoke(); }); From 2d58feebb1ff4050f522caeb053ce4ca819cdfb7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 17:54:21 +0900 Subject: [PATCH 38/43] Guard against potential null `CurrentItem` in `ParticipantPanel` --- .../OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 96a665f33d..128b9a1640 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -189,7 +189,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants var currentItem = Playlist.GetCurrentItem(); Debug.Assert(currentItem != null); - var ruleset = rulesets.GetRuleset(currentItem.RulesetID)?.CreateInstance(); + var ruleset = currentItem != null ? rulesets.GetRuleset(currentItem.RulesetID)?.CreateInstance() : null; int? currentModeRank = ruleset != null ? User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank : null; userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; From 2938f44e6c0307293b4b2f32c26d7f352a5dfe4b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 23:40:46 +0900 Subject: [PATCH 39/43] Update `PresentExternally` usages in line with framework changes --- osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs | 2 +- osu.Game/IO/WrappedStorage.cs | 4 ++-- osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs b/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs index 93cfa9634e..f0aa857769 100644 --- a/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs +++ b/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs @@ -26,7 +26,7 @@ namespace osu.Game.Tournament.Screens.Setup dropdown.Current.BindValueChanged(v => Button.Enabled.Value = v.NewValue != startupTournament, true); Action = () => game.GracefullyExit(); - folderButton.Action = storage.PresentExternally; + folderButton.Action = () => storage.PresentExternally(); ButtonText = "Close osu!"; } diff --git a/osu.Game/IO/WrappedStorage.cs b/osu.Game/IO/WrappedStorage.cs index 6f0f898de3..a6605de1d2 100644 --- a/osu.Game/IO/WrappedStorage.cs +++ b/osu.Game/IO/WrappedStorage.cs @@ -70,9 +70,9 @@ namespace osu.Game.IO public override Stream GetStream(string path, FileAccess access = FileAccess.Read, FileMode mode = FileMode.OpenOrCreate) => UnderlyingStorage.GetStream(MutatePath(path), access, mode); - public override void OpenFileExternally(string filename) => UnderlyingStorage.OpenFileExternally(MutatePath(filename)); + public override bool OpenFileExternally(string filename) => UnderlyingStorage.OpenFileExternally(MutatePath(filename)); - public override void PresentFileExternally(string filename) => UnderlyingStorage.PresentFileExternally(MutatePath(filename)); + public override bool PresentFileExternally(string filename) => UnderlyingStorage.PresentFileExternally(MutatePath(filename)); public override Storage GetStorageForDirectory(string path) { diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index 158d8811b5..0b4eca6379 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -68,7 +68,7 @@ namespace osu.Game.Overlays.Settings.Sections.General Add(new SettingsButton { Text = GeneralSettingsStrings.OpenOsuFolder, - Action = storage.PresentExternally, + Action = () => storage.PresentExternally(), }); Add(new SettingsButton From b04ca111c6b4f526dc38f172e81275fc7641db3b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 22:28:26 +0900 Subject: [PATCH 40/43] Allow realm subscriptions to be initiated from a non-update thread --- osu.Game/Database/RealmAccess.cs | 30 +++++++++++++++++------------- osu.Game/OsuGameBase.cs | 2 +- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index f0d4011ab8..8574002436 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System; using System.Collections.Generic; using System.ComponentModel; @@ -17,6 +19,7 @@ using osu.Framework.Input.Bindings; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Statistics; +using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Input.Bindings; @@ -28,8 +31,6 @@ using osu.Game.Stores; using Realms; using Realms.Exceptions; -#nullable enable - namespace osu.Game.Database { /// @@ -46,6 +47,8 @@ namespace osu.Game.Database private readonly IDatabaseContextFactory? efContextFactory; + private readonly SynchronizationContext? updateThreadSyncContext; + /// /// Version history: /// 6 ~2021-10-18 First tracked version. @@ -143,12 +146,15 @@ namespace osu.Game.Database /// /// The game storage which will be used to create the realm backing file. /// The filename to use for the realm backing file. A ".realm" extension will be added automatically if not specified. + /// The game update thread, used to post realm operations into a thread-safe context. /// An EF factory used only for migration purposes. - public RealmAccess(Storage storage, string filename, IDatabaseContextFactory? efContextFactory = null) + public RealmAccess(Storage storage, string filename, GameThread? updateThread = null, IDatabaseContextFactory? efContextFactory = null) { this.storage = storage; this.efContextFactory = efContextFactory; + updateThreadSyncContext = updateThread?.SynchronizationContext ?? SynchronizationContext.Current; + Filename = filename; if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal)) @@ -379,9 +385,6 @@ namespace osu.Game.Database public IDisposable RegisterForNotifications(Func> query, NotificationCallbackDelegate callback) where T : RealmObjectBase { - if (!ThreadSafety.IsUpdateThread) - throw new InvalidOperationException(@$"{nameof(RegisterForNotifications)} must be called from the update thread."); - lock (realmLock) { Func action = realm => query(realm).QueryAsyncWithNotifications(callback); @@ -459,23 +462,24 @@ namespace osu.Game.Database /// An which should be disposed to unsubscribe any inner subscription. public IDisposable RegisterCustomSubscription(Func action) { - if (!ThreadSafety.IsUpdateThread) - throw new InvalidOperationException(@$"{nameof(RegisterForNotifications)} must be called from the update thread."); - - var syncContext = SynchronizationContext.Current; + if (updateThreadSyncContext == null) + throw new InvalidOperationException("Attempted to register a realm subscription before update thread registration."); total_subscriptions.Value++; - registerSubscription(action); + if (ThreadSafety.IsUpdateThread) + updateThreadSyncContext.Send(_ => registerSubscription(action), null); + else + updateThreadSyncContext.Post(_ => registerSubscription(action), null); // This token is returned to the consumer. // When disposed, it will cause the registration to be permanently ceased (unsubscribed with realm and unregistered by this class). return new InvokeOnDisposal(() => { if (ThreadSafety.IsUpdateThread) - syncContext.Send(_ => unsubscribe(), null); + updateThreadSyncContext.Send(_ => unsubscribe(), null); else - syncContext.Post(_ => unsubscribe(), null); + updateThreadSyncContext.Post(_ => unsubscribe(), null); void unsubscribe() { diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 5468db348e..7b9aca4086 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -200,7 +200,7 @@ namespace osu.Game if (Storage.Exists(DatabaseContextFactory.DATABASE_NAME)) dependencies.Cache(EFContextFactory = new DatabaseContextFactory(Storage)); - dependencies.Cache(realm = new RealmAccess(Storage, "client", EFContextFactory)); + dependencies.Cache(realm = new RealmAccess(Storage, "client", Host.UpdateThread, EFContextFactory)); dependencies.CacheAs(RulesetStore = new RealmRulesetStore(realm, Storage)); dependencies.CacheAs(RulesetStore); From 878e8d21a3925bd4fc1cf4339ca6b346643bbac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 24 Mar 2022 21:51:10 +0100 Subject: [PATCH 41/43] Remove assertion to fix "expression always true" inspection --- .../OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 128b9a1640..70f4030b79 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -187,8 +186,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants const double fade_time = 50; var currentItem = Playlist.GetCurrentItem(); - Debug.Assert(currentItem != null); - var ruleset = currentItem != null ? rulesets.GetRuleset(currentItem.RulesetID)?.CreateInstance() : null; int? currentModeRank = ruleset != null ? User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank : null; From b4c0155b3d22f2115fc250ef3451db9e1b2fe273 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Mar 2022 13:07:21 +0900 Subject: [PATCH 42/43] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 1b5461959a..182495cd56 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 1c1deaae8e..4193dc8fa0 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 23101c5af6..f97f15d091 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,7 +61,7 @@ - + @@ -84,7 +84,7 @@ - + From 09c5325b08bfa8bb9c993c3428b5d79e65c91ed2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Mar 2022 13:18:49 +0900 Subject: [PATCH 43/43] Update resources --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 182495cd56..6a3b113fa2 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 4193dc8fa0..3c01f29671 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index f97f15d091..c8f170497d 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -62,7 +62,7 @@ - +