diff --git a/osu.Game.Tests/Online/Matchmaking/MatchmakingRoomStateTest.cs b/osu.Game.Tests/Online/Matchmaking/MatchmakingRoomStateTest.cs index c9219c871a..5f82d22ae8 100644 --- a/osu.Game.Tests/Online/Matchmaking/MatchmakingRoomStateTest.cs +++ b/osu.Game.Tests/Online/Matchmaking/MatchmakingRoomStateTest.cs @@ -29,17 +29,17 @@ namespace osu.Game.Tests.Online.Matchmaking new SoloScoreInfo { UserID = 3, TotalScore = 750 }, ], placement_points); - Assert.AreEqual(8, state.Users[1].Points); - Assert.AreEqual(1, state.Users[1].Placement); - Assert.AreEqual(1, state.Users[1].Rounds[1].Placement); + Assert.AreEqual(8, state.Users.GetOrAdd(1).Points); + Assert.AreEqual(1, state.Users.GetOrAdd(1).Placement); + Assert.AreEqual(1, state.Users.GetOrAdd(1).Rounds.GetOrAdd(1).Placement); - Assert.AreEqual(6, state.Users[2].Points); - Assert.AreEqual(3, state.Users[2].Placement); - Assert.AreEqual(3, state.Users[2].Rounds[1].Placement); + Assert.AreEqual(6, state.Users.GetOrAdd(2).Points); + Assert.AreEqual(3, state.Users.GetOrAdd(2).Placement); + Assert.AreEqual(3, state.Users.GetOrAdd(2).Rounds.GetOrAdd(1).Placement); - Assert.AreEqual(7, state.Users[3].Points); - Assert.AreEqual(2, state.Users[3].Placement); - Assert.AreEqual(2, state.Users[3].Rounds[1].Placement); + Assert.AreEqual(7, state.Users.GetOrAdd(3).Points); + Assert.AreEqual(2, state.Users.GetOrAdd(3).Placement); + Assert.AreEqual(2, state.Users.GetOrAdd(3).Rounds.GetOrAdd(1).Placement); // 2 -> 1 -> 3 @@ -51,17 +51,17 @@ namespace osu.Game.Tests.Online.Matchmaking new SoloScoreInfo { UserID = 3, TotalScore = 500 }, ], placement_points); - Assert.AreEqual(15, state.Users[1].Points); - Assert.AreEqual(1, state.Users[1].Placement); - Assert.AreEqual(2, state.Users[1].Rounds[2].Placement); + Assert.AreEqual(15, state.Users.GetOrAdd(1).Points); + Assert.AreEqual(1, state.Users.GetOrAdd(1).Placement); + Assert.AreEqual(2, state.Users.GetOrAdd(1).Rounds.GetOrAdd(2).Placement); - Assert.AreEqual(14, state.Users[2].Points); - Assert.AreEqual(2, state.Users[2].Placement); - Assert.AreEqual(1, state.Users[2].Rounds[2].Placement); + Assert.AreEqual(14, state.Users.GetOrAdd(2).Points); + Assert.AreEqual(2, state.Users.GetOrAdd(2).Placement); + Assert.AreEqual(1, state.Users.GetOrAdd(2).Rounds.GetOrAdd(2).Placement); - Assert.AreEqual(13, state.Users[3].Points); - Assert.AreEqual(3, state.Users[3].Placement); - Assert.AreEqual(3, state.Users[3].Rounds[2].Placement); + Assert.AreEqual(13, state.Users.GetOrAdd(3).Points); + Assert.AreEqual(3, state.Users.GetOrAdd(3).Placement); + Assert.AreEqual(3, state.Users.GetOrAdd(3).Rounds.GetOrAdd(2).Placement); } [Test] @@ -80,21 +80,21 @@ namespace osu.Game.Tests.Online.Matchmaking new SoloScoreInfo { UserID = 4, TotalScore = 500 }, ], placement_points); - Assert.AreEqual(7, state.Users[1].Points); - Assert.AreEqual(1, state.Users[1].Placement); - Assert.AreEqual(2, state.Users[1].Rounds[1].Placement); + Assert.AreEqual(7, state.Users.GetOrAdd(1).Points); + Assert.AreEqual(1, state.Users.GetOrAdd(1).Placement); + Assert.AreEqual(2, state.Users.GetOrAdd(1).Rounds.GetOrAdd(1).Placement); - Assert.AreEqual(7, state.Users[2].Points); - Assert.AreEqual(2, state.Users[2].Placement); - Assert.AreEqual(2, state.Users[2].Rounds[1].Placement); + Assert.AreEqual(7, state.Users.GetOrAdd(2).Points); + Assert.AreEqual(2, state.Users.GetOrAdd(2).Placement); + Assert.AreEqual(2, state.Users.GetOrAdd(2).Rounds.GetOrAdd(1).Placement); - Assert.AreEqual(5, state.Users[3].Points); - Assert.AreEqual(3, state.Users[3].Placement); - Assert.AreEqual(4, state.Users[3].Rounds[1].Placement); + Assert.AreEqual(5, state.Users.GetOrAdd(3).Points); + Assert.AreEqual(3, state.Users.GetOrAdd(3).Placement); + Assert.AreEqual(4, state.Users.GetOrAdd(3).Rounds.GetOrAdd(1).Placement); - Assert.AreEqual(5, state.Users[4].Points); - Assert.AreEqual(4, state.Users[4].Placement); - Assert.AreEqual(4, state.Users[4].Rounds[1].Placement); + Assert.AreEqual(5, state.Users.GetOrAdd(4).Points); + Assert.AreEqual(4, state.Users.GetOrAdd(4).Placement); + Assert.AreEqual(4, state.Users.GetOrAdd(4).Rounds.GetOrAdd(1).Placement); } [Test] @@ -120,8 +120,8 @@ namespace osu.Game.Tests.Online.Matchmaking new SoloScoreInfo { UserID = 2, TotalScore = 1000 }, ], placement_points); - Assert.AreEqual(1, state.Users[1].Placement); - Assert.AreEqual(2, state.Users[2].Placement); + Assert.AreEqual(1, state.Users.GetOrAdd(1).Placement); + Assert.AreEqual(2, state.Users.GetOrAdd(2).Placement); } [Test] @@ -142,12 +142,12 @@ namespace osu.Game.Tests.Online.Matchmaking new SoloScoreInfo { UserID = 5, TotalScore = 1000 }, ], placement_points); - Assert.AreEqual(1, state.Users[1].Placement); - Assert.AreEqual(2, state.Users[2].Placement); - Assert.AreEqual(3, state.Users[3].Placement); - Assert.AreEqual(4, state.Users[4].Placement); - Assert.AreEqual(5, state.Users[5].Placement); - Assert.AreEqual(6, state.Users[6].Placement); + Assert.AreEqual(1, state.Users.GetOrAdd(1).Placement); + Assert.AreEqual(2, state.Users.GetOrAdd(2).Placement); + Assert.AreEqual(3, state.Users.GetOrAdd(3).Placement); + Assert.AreEqual(4, state.Users.GetOrAdd(4).Placement); + Assert.AreEqual(5, state.Users.GetOrAdd(5).Placement); + Assert.AreEqual(6, state.Users.GetOrAdd(6).Placement); } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableKeyCounter.cs index 098f8e3246..8e9df5b2bf 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableKeyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableKeyCounter.cs @@ -35,7 +35,9 @@ namespace osu.Game.Tests.Visual.Gameplay protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset(); - protected override Drawable CreateDefaultImplementation() => new ArgonKeyCounterDisplay(); + protected override Drawable CreateArgonImplementation() => new ArgonKeyCounterDisplay(); + + protected override Drawable CreateDefaultImplementation() => new DefaultKeyCounterDisplay(); protected override Drawable CreateLegacyImplementation() => new LegacyKeyCounterDisplay(); } diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs index 02c669aaf5..01f76157f1 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs @@ -1,11 +1,13 @@ // 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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics.Cursor; using osu.Game.Online.API; +using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -62,5 +64,41 @@ namespace osu.Game.Tests.Visual.Matchmaking panel.AllowSelection = value; }); } + + [Test] + public void TestFailedBeatmapLookup() + { + AddStep("setup request handle", () => + { + var api = (DummyAPIAccess)API; + var handler = api.HandleRequest; + api.HandleRequest = req => + { + switch (req) + { + case GetBeatmapRequest: + case GetBeatmapsRequest: + req.TriggerFailure(new InvalidOperationException()); + return false; + + default: + return handler?.Invoke(req) ?? false; + } + }; + }); + + AddStep("add panel", () => + { + Child = new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.Both, + Child = new BeatmapSelectPanel(new MultiplayerPlaylistItem()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }; + }); + } } } diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs index a598ce9a39..e88b10d30d 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs @@ -124,11 +124,11 @@ namespace osu.Game.Tests.Visual.Matchmaking foreach (var user in MultiplayerClient.ServerRoom!.Users.OrderBy(_ => RNG.Next())) { - state.Users[user.UserID].Placement = i++; - state.Users[user.UserID].Points = (8 - i) * 7; - state.Users[user.UserID].Rounds[1].Placement = 1; - state.Users[user.UserID].Rounds[1].TotalScore = 1; - state.Users[user.UserID].Rounds[1].Statistics[HitResult.LargeBonus] = 1; + state.Users.GetOrAdd(user.UserID).Placement = i++; + state.Users.GetOrAdd(user.UserID).Points = (8 - i) * 7; + state.Users.GetOrAdd(user.UserID).Rounds.GetOrAdd(1).Placement = 1; + state.Users.GetOrAdd(user.UserID).Rounds.GetOrAdd(1).TotalScore = 1; + state.Users.GetOrAdd(user.UserID).Rounds.GetOrAdd(1).Statistics[HitResult.LargeBonus] = 1; } }); } diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs index c2b2b95d55..16f15014fb 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs @@ -153,7 +153,7 @@ namespace osu.Game.Tests.Visual.Matchmaking MatchmakingRoomState state = new MatchmakingRoomState(); for (int i = 0; i < room.Users.Count; i++) - state.Users[room.Users[i].UserID].Placement = placements[i]; + state.Users.GetOrAdd(room.Users[i].UserID).Placement = placements[i]; MultiplayerClient.ChangeMatchRoomState(state).WaitSafely(); }); diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs index 4d1a40cc10..c286ca8664 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs @@ -36,28 +36,28 @@ namespace osu.Game.Tests.Visual.Matchmaking int localUserId = API.LocalUser.Value.OnlineID; // Overall state. - state.Users[localUserId].Placement = 1; - state.Users[localUserId].Points = 8; + state.Users.GetOrAdd(localUserId).Placement = 1; + state.Users.GetOrAdd(localUserId).Points = 8; for (int round = 1; round <= state.CurrentRound; round++) - state.Users[localUserId].Rounds[round].Placement = round; + state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(round).Placement = round; // Highest score. - state.Users[localUserId].Rounds[1].TotalScore = 1000; + state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(1).TotalScore = 1000; // Highest accuracy. - state.Users[localUserId].Rounds[2].Accuracy = 0.9995; + state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(2).Accuracy = 0.9995; // Highest combo. - state.Users[localUserId].Rounds[3].MaxCombo = 100; + state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(3).MaxCombo = 100; // Most bonus score. - state.Users[localUserId].Rounds[4].Statistics[HitResult.LargeBonus] = 50; + state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(4).Statistics[HitResult.LargeBonus] = 50; // Smallest score difference. - state.Users[localUserId].Rounds[5].TotalScore = 1000; + state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(5).TotalScore = 1000; // Largest score difference. - state.Users[localUserId].Rounds[6].TotalScore = 1000; + state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(6).TotalScore = 1000; MultiplayerClient.ChangeMatchRoomState(state).WaitSafely(); }); @@ -103,36 +103,51 @@ namespace osu.Game.Tests.Visual.Matchmaking int localUserId = API.LocalUser.Value.OnlineID; // Overall state. - state.Users[localUserId].Placement = 1; - state.Users[localUserId].Points = 8; - state.Users[invalid_user_id].Placement = 2; - state.Users[invalid_user_id].Points = 7; + state.Users.GetOrAdd(localUserId).Placement = 1; + state.Users.GetOrAdd(localUserId).Points = 8; + state.Users.GetOrAdd(invalid_user_id).Placement = 2; + state.Users.GetOrAdd(invalid_user_id).Points = 7; for (int round = 1; round <= state.CurrentRound; round++) - state.Users[localUserId].Rounds[round].Placement = round; + state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(round).Placement = round; // Highest score. - state.Users[localUserId].Rounds[1].TotalScore = 1000; - state.Users[invalid_user_id].Rounds[1].TotalScore = 990; + state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(1).TotalScore = 1000; + state.Users.GetOrAdd(invalid_user_id).Rounds.GetOrAdd(1).TotalScore = 990; // Highest accuracy. - state.Users[localUserId].Rounds[2].Accuracy = 0.9995; - state.Users[invalid_user_id].Rounds[2].Accuracy = 0.5; + state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(2).Accuracy = 0.9995; + state.Users.GetOrAdd(invalid_user_id).Rounds.GetOrAdd(2).Accuracy = 0.5; // Highest combo. - state.Users[localUserId].Rounds[3].MaxCombo = 100; - state.Users[invalid_user_id].Rounds[3].MaxCombo = 10; + state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(3).MaxCombo = 100; + state.Users.GetOrAdd(invalid_user_id).Rounds.GetOrAdd(3).MaxCombo = 10; // Most bonus score. - state.Users[localUserId].Rounds[4].Statistics[HitResult.LargeBonus] = 50; - state.Users[invalid_user_id].Rounds[4].Statistics[HitResult.LargeBonus] = 25; + state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(4).Statistics[HitResult.LargeBonus] = 50; + state.Users.GetOrAdd(invalid_user_id).Rounds.GetOrAdd(4).Statistics[HitResult.LargeBonus] = 25; // Smallest score difference. - state.Users[localUserId].Rounds[5].TotalScore = 1000; - state.Users[invalid_user_id].Rounds[5].TotalScore = 999; + state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(5).TotalScore = 1000; + state.Users.GetOrAdd(invalid_user_id).Rounds.GetOrAdd(5).TotalScore = 999; // Largest score difference. - state.Users[localUserId].Rounds[6].TotalScore = 1000; - state.Users[invalid_user_id].Rounds[6].TotalScore = 0; + state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(6).TotalScore = 1000; + state.Users.GetOrAdd(invalid_user_id).Rounds.GetOrAdd(6).TotalScore = 0; + + MultiplayerClient.ChangeMatchRoomState(state).WaitSafely(); + }); + } + + [Test] + public void TestNoUsers() + { + AddStep("show results with no users", () => + { + var state = new MatchmakingRoomState + { + CurrentRound = 6, + Stage = MatchmakingStage.Ended + }; MultiplayerClient.ChangeMatchRoomState(state).WaitSafely(); }); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDrawableDate.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDrawableDate.cs index b590abf4e5..e78b4d2496 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDrawableDate.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDrawableDate.cs @@ -2,9 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; using osu.Game.Graphics; using osuTK; using osuTK.Graphics; @@ -13,25 +16,35 @@ namespace osu.Game.Tests.Visual.UserInterface { public partial class TestSceneDrawableDate : OsuTestScene { - public TestSceneDrawableDate() + [SetUpSteps] + public void SetUpSteps() { - Child = new FillFlowContainer + AddStep("Create 7 dates", () => { - Direction = FillDirection.Vertical, - AutoSizeAxes = Axes.Both, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Children = new Drawable[] + Child = new FillFlowContainer { - new PokeyDrawableDate(DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(60))), - new PokeyDrawableDate(DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(55))), - new PokeyDrawableDate(DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(50))), - new PokeyDrawableDate(DateTimeOffset.Now), - new PokeyDrawableDate(DateTimeOffset.Now.Add(TimeSpan.FromSeconds(60))), - new PokeyDrawableDate(DateTimeOffset.Now.Add(TimeSpan.FromSeconds(65))), - new PokeyDrawableDate(DateTimeOffset.Now.Add(TimeSpan.FromSeconds(70))), - } - }; + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Both, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Children = new Drawable[] + { + new PokeyDrawableDate(DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(60))), + new PokeyDrawableDate(DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(55))), + new PokeyDrawableDate(DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(50))), + new PokeyDrawableDate(DateTimeOffset.Now), + new PokeyDrawableDate(DateTimeOffset.Now.Add(TimeSpan.FromSeconds(60))), + new PokeyDrawableDate(DateTimeOffset.Now.Add(TimeSpan.FromSeconds(65))), + new PokeyDrawableDate(DateTimeOffset.Now.Add(TimeSpan.FromSeconds(70))), + } + }; + }); + } + + [Test] + public void TestSecondsUpdate() + { + AddUntilStep("4th date says \"2 seconds ago\"", () => this.ChildrenOfType().ElementAt(3).Current.Value == "2 seconds ago"); } private partial class PokeyDrawableDate : CompositeDrawable diff --git a/osu.Game/Graphics/DrawableDate.cs b/osu.Game/Graphics/DrawableDate.cs index 7af4df2d25..e5383bf3a9 100644 --- a/osu.Game/Graphics/DrawableDate.cs +++ b/osu.Game/Graphics/DrawableDate.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; using osu.Game.Utils; @@ -80,7 +81,7 @@ namespace osu.Game.Graphics public DateTimeOffset TooltipContent => Date; - private class HumanisedDate : IEquatable, ILocalisableStringData + private class HumanisedDate : ILocalisableStringData { public readonly DateTimeOffset Date; @@ -89,11 +90,18 @@ namespace osu.Game.Graphics Date = date; } - public bool Equals(HumanisedDate? other) - => other?.Date != null && Date.Equals(other.Date); - - public bool Equals(ILocalisableStringData? other) - => other is HumanisedDate otherDate && Equals(otherDate); + /// + /// Humanizer formats the relative to the local computer time. + /// Therefore, replacing a instance with another instance of the class with the same + /// should have the effect of replacing and re-formatting the text. + /// Including in equality members would stop this from happening, as + /// has equality-based early guards to prevent redundant text replaces. + /// Thus, instances of these class just compare to any to ensure re-formatting happens correctly. + /// There are "technically" more "correct" ways to do this (like also including the current time into equality checks), + /// but they are simultaneously functionally equivalent to this and overly convoluted. + /// This is a private hack-job of a wrapper around humanizer anyway. + /// + public bool Equals(ILocalisableStringData? other) => false; public string GetLocalised(LocalisationParameters parameters) => HumanizerUtils.Humanize(Date); diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs index 6402962e85..75b0187388 100644 --- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs +++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs @@ -89,13 +89,13 @@ namespace osu.Game.Online.Metadata userStatus.BindValueChanged(status => { if (localUser.Value is not GuestUser) - UpdateStatus(status.NewValue); + UpdateStatus(status.NewValue).FireAndForget(); }, true); userActivity.BindValueChanged(activity => { if (localUser.Value is not GuestUser) - UpdateActivity(activity.NewValue); + UpdateActivity(activity.NewValue).FireAndForget(); }, true); } @@ -121,8 +121,8 @@ namespace osu.Game.Online.Metadata if (localUser.Value is not GuestUser) { - UpdateActivity(userActivity.Value); - UpdateStatus(userStatus.Value); + UpdateActivity(userActivity.Value).FireAndForget(); + UpdateStatus(userStatus.Value).FireAndForget(); } if (lastQueueId.Value >= 0) diff --git a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoomState.cs b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoomState.cs index 9e1953fc59..b55fa63844 100644 --- a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoomState.cs +++ b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoomState.cs @@ -81,10 +81,10 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking foreach (var score in scoreGroup) { - MatchmakingUser mmUser = Users[score.UserID]; + MatchmakingUser mmUser = Users.GetOrAdd(score.UserID); mmUser.Points += placementPoints[placement - 1]; - MatchmakingRound mmRound = mmUser.Rounds[CurrentRound]; + MatchmakingRound mmRound = mmUser.Rounds.GetOrAdd(CurrentRound); mmRound.Placement = placement; mmRound.TotalScore = score.TotalScore; mmRound.Accuracy = score.Accuracy; diff --git a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoundList.cs b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoundList.cs index c34d1771f8..fb9a713c10 100644 --- a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoundList.cs +++ b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingRoundList.cs @@ -21,27 +21,24 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking [Key(0)] public IDictionary RoundsDictionary { get; set; } = new Dictionary(); - /// - /// Creates or retrieves the score for the given round. - /// - /// The round. - public MatchmakingRound this[int round] - { - get - { - if (RoundsDictionary.TryGetValue(round, out MatchmakingRound? score)) - return score; - - return RoundsDictionary[round] = new MatchmakingRound { Round = round }; - } - } - /// /// The total number of rounds. /// [IgnoreMember] public int Count => RoundsDictionary.Count; + /// + /// Retrieves or adds a entry to this list. + /// + /// The round. + public MatchmakingRound GetOrAdd(int round) + { + if (RoundsDictionary.TryGetValue(round, out MatchmakingRound? score)) + return score; + + return RoundsDictionary[round] = new MatchmakingRound { Round = round }; + } + public IEnumerator GetEnumerator() => RoundsDictionary.Values.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); diff --git a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUser.cs b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUser.cs index f596f2473e..ac97b114d8 100644 --- a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUser.cs +++ b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUser.cs @@ -23,7 +23,7 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking /// The aggregate room placement (1-based). /// [Key(1)] - public int Placement { get; set; } + public int? Placement { get; set; } /// /// The aggregate points. diff --git a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUserList.cs b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUserList.cs index 600134de4e..23a246db5d 100644 --- a/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUserList.cs +++ b/osu.Game/Online/Multiplayer/MatchTypes/Matchmaking/MatchmakingUserList.cs @@ -21,27 +21,24 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking [Key(0)] public IDictionary UserDictionary { get; set; } = new Dictionary(); - /// - /// Creates or retrieves the user for the given id. - /// - /// The user id. - public MatchmakingUser this[int userId] - { - get - { - if (UserDictionary.TryGetValue(userId, out MatchmakingUser? user)) - return user; - - return UserDictionary[userId] = new MatchmakingUser { UserId = userId }; - } - } - /// /// The total number of users. /// [IgnoreMember] public int Count => UserDictionary.Count; + /// + /// Retrieves or adds a entry to this list. + /// + /// The user ID. + public MatchmakingUser GetOrAdd(int userId) + { + if (UserDictionary.TryGetValue(userId, out MatchmakingUser? user)) + return user; + + return UserDictionary[userId] = new MatchmakingUser { UserId = userId }; + } + public IEnumerator GetEnumerator() => UserDictionary.Values.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index a58d433e7d..44cbbafe72 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -201,7 +201,7 @@ namespace osu.Game.Online.Multiplayer if (!connected.NewValue) { if (Room != null) - LeaveRoom(); + LeaveRoom().FireAndForget(); MatchmakingQueueLeft?.Invoke(); } @@ -560,7 +560,7 @@ namespace osu.Game.Online.Multiplayer return; if (user.Equals(LocalUser)) - LeaveRoom(); + LeaveRoom().FireAndForget(); handleUserLeft(user, UserKicked); }); diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 7f09fbdc9e..f245e8cf3a 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Online.API; +using osu.Game.Online.Multiplayer; using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; @@ -203,7 +204,7 @@ namespace osu.Game.Online.Spectator Task IStatefulUserHubClient.DisconnectRequested() { - Schedule(() => DisconnectInternal()); + Schedule(() => DisconnectInternal().FireAndForget()); return Task.CompletedTask; } @@ -290,7 +291,7 @@ namespace osu.Game.Online.Spectator else currentState.State = SpectatedUserState.Quit; - EndPlayingInternal(currentState); + EndPlayingInternal(currentState).FireAndForget(); }); } @@ -304,7 +305,7 @@ namespace osu.Game.Online.Spectator return; } - WatchUserInternal(userId); + WatchUserInternal(userId).FireAndForget(); } public void StopWatchingUser(int userId) @@ -321,7 +322,7 @@ namespace osu.Game.Online.Spectator watchedUsersRefCounts.Remove(userId); watchedUserStates.Remove(userId); - StopWatchingUserInternal(userId); + StopWatchingUserInternal(userId).FireAndForget(); }); } diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index f56e5e6ac3..7ef2fffeda 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -162,16 +162,17 @@ namespace osu.Game.Overlays private int runningDepth; private readonly Scheduler postScheduler = new Scheduler(); + private readonly Scheduler criticalPostScheduler = new Scheduler(); public override bool IsPresent => // Delegate presence as we need to consider the toast tray in addition to the main overlay. - State.Value == Visibility.Visible || mainContent.IsPresent || toastTray.IsPresent || postScheduler.HasPendingTasks; + State.Value == Visibility.Visible || mainContent.IsPresent || toastTray.IsPresent || postScheduler.HasPendingTasks || criticalPostScheduler.HasPendingTasks; private bool processingPosts = true; private double? lastSamplePlayback; - public void Post(Notification notification) => postScheduler.Add(() => + public void Post(Notification notification) => (notification.IsCritical ? criticalPostScheduler : postScheduler).Add(() => { ++runningDepth; @@ -220,6 +221,8 @@ namespace osu.Game.Overlays { base.Update(); + criticalPostScheduler.Update(); + if (processingPosts) postScheduler.Update(); } diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs index dd60e303f6..e66b999540 100644 --- a/osu.Game/Overlays/NotificationOverlayToastTray.cs +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -91,7 +91,12 @@ namespace osu.Game.Overlays public void FlushAllToasts() { foreach (var notification in toastFlow.ToArray()) + { + if (notification.IsCritical) + continue; + forwardNotification(notification); + } } public void Post(Notification notification) diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index ccfd1adb39..8a2a7cee81 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -39,6 +39,11 @@ namespace osu.Game.Overlays.Notifications /// public bool IsImportant { get; init; } = true; + /// + /// Critical notifications show even during gameplay or other scenarios where notifications would usually be suppressed. + /// + public bool IsCritical { get; init; } + /// /// Transient notifications only show as a toast, and do not linger in notification history. /// diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index 777ec1790c..5dbc7a55ab 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -50,7 +50,7 @@ namespace osu.Game.Screens.Footer private Box background = null!; private FillFlowContainer buttonsFlow = null!; - private Container footerContentContainer = null!; + private Container overlayContentContainer = null!; private Container hiddenButtonsContainer = null!; private LogoTrackingContainer logoTrackingContainer = null!; @@ -102,6 +102,7 @@ namespace osu.Game.Screens.Footer { buttonsFlow = new FillFlowContainer { + Name = "Visible footer buttons", Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Y = ScreenFooterButton.CORNER_RADIUS, @@ -109,8 +110,9 @@ namespace osu.Game.Screens.Footer Spacing = new Vector2(7, 0), AutoSizeAxes = Axes.Both, }, - footerContentContainer = new Container + overlayContentContainer = new Container { + Name = "Overlay-provided extra content", RelativeSizeAxes = Axes.Both, Y = -OsuGame.SCREEN_EDGE_MARGIN, }, @@ -126,6 +128,7 @@ namespace osu.Game.Screens.Footer }, hiddenButtonsContainer = new Container { + Name = "Hidden footer buttons", Margin = new MarginPadding { Left = OsuGame.SCREEN_EDGE_MARGIN + ScreenBackButton.BUTTON_WIDTH + padding }, Y = ScreenFooterButton.CORNER_RADIUS, Anchor = Anchor.BottomLeft, @@ -234,11 +237,11 @@ namespace osu.Game.Screens.Footer public ShearedOverlayContainer? ActiveOverlay { get; private set; } - private VisibilityContainer? activeFooterContent; + private VisibilityContainer? activeOverlayContent; private readonly List temporarilyHiddenButtons = new List(); - public IDisposable RegisterActiveOverlayContainer(ShearedOverlayContainer overlay, out VisibilityContainer? footerContent) + public IDisposable RegisterActiveOverlayContainer(ShearedOverlayContainer overlay, out VisibilityContainer? overlayContent) { if (ActiveOverlay != null) { @@ -267,12 +270,12 @@ namespace osu.Game.Screens.Footer updateColourScheme(overlay.ColourProvider.Hue); - footerContent = overlay.CreateFooterContent(); - activeFooterContent = footerContent; - var content = footerContent; + overlayContent = overlay.CreateFooterContent(); + activeOverlayContent = overlayContent; + var content = overlayContent; if (content != null) - footerContentContainer.Child = content; + overlayContentContainer.Child = content; if (temporarilyHiddenButtons.Count > 0) this.Delay(60).Schedule(() => content?.Show()); @@ -287,15 +290,19 @@ namespace osu.Game.Screens.Footer if (ActiveOverlay == null) return; - Debug.Assert(activeFooterContent != null); - activeFooterContent.Hide(); + Debug.Assert(activeOverlayContent != null); + activeOverlayContent.Hide(); - double timeUntilRun = activeFooterContent.LatestTransformEndTime - Time.Current; + double timeUntilRun = activeOverlayContent.LatestTransformEndTime - Time.Current; for (int i = 0; i < temporarilyHiddenButtons.Count; i++) { var button = temporarilyHiddenButtons[i]; hiddenButtonsContainer.Remove(button, false); + // temporarily bypass autosize on the X axis to prevent the buttons taking space + // immediately upon being moved back to the flow. + // this prevents the overlay content jumping to the right during its fade-out. + button.BypassAutoSizeAxes = Axes.X; buttonsFlow.Add(button); makeButtonAppearFromBottom(button, 0); @@ -305,8 +312,13 @@ namespace osu.Game.Screens.Footer updateColourScheme(OverlayColourScheme.Aquamarine.GetHue()); - activeFooterContent.Delay(timeUntilRun).Expire(); - activeFooterContent = null; + activeOverlayContent.Delay(timeUntilRun).Schedule(() => + { + // overlay content is done displaying, re-enable autosize on all active buttons + foreach (var button in buttonsFlow) + button.BypassAutoSizeAxes = Axes.None; + }).Expire(); + activeOverlayContent = null; ActiveOverlay = null; } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs index 001804a521..aa0329ad94 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs @@ -111,7 +111,17 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { Debug.Assert(card == null); - var beatmap = b.GetResultSafely()!; + APIBeatmap beatmap = b.GetResultSafely() ?? new APIBeatmap + { + BeatmapSet = new APIBeatmapSet + { + Title = "unknown beatmap", + TitleUnicode = "unknown beatmap", + Artist = "unknown artist", + ArtistUnicode = "unknown artist", + } + }; + beatmap.StarRating = Item.StarRating; mainContent.Add(card = new BeatmapCardMatchmaking(beatmap) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs index e86a546533..e2455eb020 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs @@ -414,8 +414,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match if (!matchmakingState.Users.UserDictionary.TryGetValue(User.Id, out MatchmakingUser? userScore)) return; - rankText.Text = userScore.Placement.Ordinalize(CultureInfo.CurrentCulture); - rankText.FadeColour(SubScreenResults.ColourForPlacement(userScore.Placement)); + if (userScore.Placement == null) + return; + + rankText.Text = userScore.Placement.Value.Ordinalize(CultureInfo.CurrentCulture); + rankText.FadeColour(SubScreenResults.ColourForPlacement(userScore.Placement.Value)); scoreText.Text = $"{userScore.Points} pts"; }); diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs index 9fb5d258a8..4b97400ebe 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs @@ -239,8 +239,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match if (client.Room?.MatchState is not MatchmakingRoomState matchmakingState) continue; - if (matchmakingState.Users.UserDictionary.TryGetValue(panels[i].User.Id, out MatchmakingUser? user)) - SetLayoutPosition(Children[i], user.Placement); + if (matchmakingState.Users.UserDictionary.TryGetValue(panels[i].User.Id, out MatchmakingUser? user) && user.Placement != null) + SetLayoutPosition(Children[i], user.Placement.Value); else SetLayoutPosition(Children[i], float.MaxValue); } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/SubScreenResults.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/SubScreenResults.cs index 797519a53c..6ba6b3f4b0 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/SubScreenResults.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/SubScreenResults.cs @@ -194,20 +194,25 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results { userStatistics.Clear(); - if (state.Users[client.LocalUser!.UserID].Rounds.Count == 0) + var localUserState = state.Users.GetOrAdd(client.LocalUser!.UserID); + + if (localUserState.Rounds.Count == 0) { placementText.Text = "-"; placementText.Colour = OsuColour.Gray(1f); return; } - int overallPlacement = state.Users[client.LocalUser!.UserID].Placement; + int? overallPlacement = localUserState.Placement; - placementText.Text = overallPlacement.Ordinalize(CultureInfo.CurrentCulture); - placementText.Colour = ColourForPlacement(overallPlacement); + if (overallPlacement != null) + { + placementText.Text = overallPlacement.Value.Ordinalize(CultureInfo.CurrentCulture); + placementText.Colour = ColourForPlacement(overallPlacement.Value); - int overallPoints = state.Users[client.LocalUser!.UserID].Points; - addStatistic(overallPlacement, $"Overall position ({overallPoints} points)"); + int overallPoints = localUserState.Points; + addStatistic(overallPlacement.Value, $"Overall position ({overallPoints} points)"); + } var accuracyOrderedUsers = state.Users.Select(u => (user: u, avgAcc: u.Rounds.Select(r => r.Accuracy).DefaultIfEmpty(0).Average())) .OrderByDescending(t => t.avgAcc) @@ -223,7 +228,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results int maxComboPlacement = maxComboOrderedUsers.index + 1; addStatistic(maxComboPlacement, $"Best max combo ({maxComboOrderedUsers.info.maxCombo}x)"); - var bestPlacement = state.Users[client.LocalUser!.UserID].Rounds.MinBy(r => r.Placement); + var bestPlacement = localUserState.Rounds.MinBy(r => r.Placement); addStatistic(bestPlacement!.Placement, $"Best round placement (round {bestPlacement.Round})"); void addStatistic(int position, string text) => userStatistics.Add(new PanelUserStatistic(position, text)); @@ -255,27 +260,27 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results roomAwards.Clear(); long maxScore = long.MinValue; - int maxScoreUserId = 0; + int maxScoreUserId = -1; double maxAccuracy = double.MinValue; - int maxAccuracyUserId = 0; + int maxAccuracyUserId = -1; int maxCombo = int.MinValue; - int maxComboUserId = 0; + int maxComboUserId = -1; long maxBonusScore = 0; - int maxBonusScoreUserId = 0; + int maxBonusScoreUserId = -1; long largestScoreDifference = long.MinValue; - int largestScoreDifferenceUserId = 0; + int largestScoreDifferenceUserId = -1; long smallestScoreDifference = long.MaxValue; - int smallestScoreDifferenceUserId = 0; + int smallestScoreDifferenceUserId = -1; for (int round = 1; round <= state.CurrentRound; round++) { long roundHighestScore = long.MinValue; - int roundHighestScoreUserId = 0; + int roundHighestScoreUserId = -1; long roundLowestScore = long.MaxValue; @@ -344,11 +349,14 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results } } - addAward(maxScoreUserId, "Score champ", "Highest score in a single round"); + if (maxScoreUserId > 0) + addAward(maxScoreUserId, "Score champ", "Highest score in a single round"); - addAward(maxAccuracyUserId, "Most accurate", "Highest accuracy in a single round"); + if (maxAccuracyUserId > 0) + addAward(maxAccuracyUserId, "Most accurate", "Highest accuracy in a single round"); - addAward(maxComboUserId, "Top combo", "Highest combo in a single round"); + if (maxComboUserId > 0) + addAward(maxComboUserId, "Top combo", "Highest combo in a single round"); if (maxBonusScoreUserId > 0) addAward(maxBonusScoreUserId, "Biggest bonus", "Biggest bonus score across all rounds"); diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs index 95e3cb0236..527b1ba243 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs @@ -392,7 +392,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match return; } - client.ChangeState(MultiplayerUserState.Idle); + client.ChangeState(MultiplayerUserState.Idle).FireAndForget(); } /// diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs index 40ac0e5777..468e024a65 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs @@ -2,10 +2,15 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Sprites; using osu.Framework.Screens; +using osu.Game.Graphics; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -32,11 +37,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue [Resolved] private INotificationOverlay? notifications { get; set; } - [Resolved] - private IPerformFromScreenRunner? performer { get; set; } - - private ProgressNotification? backgroundNotification; - private Notification? readyNotification; + private BackgroundQueueNotification? backgroundNotification; private bool isBackgrounded; protected override void LoadComplete() @@ -118,27 +119,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue if (backgroundNotification != null) return; - notifications?.Post(backgroundNotification = new ProgressNotification - { - Text = "Searching for opponents...", - CompletionTarget = n => notifications.Post(readyNotification = n), - CompletionText = "Your match is ready! Click to join.", - CompletionClickAction = () => - { - client.MatchmakingAcceptInvitation().FireAndForget(); - performer?.PerformFromScreen(s => s.Push(new IntroScreen())); - - closeNotifications(); - return true; - }, - CancelRequested = () => - { - client.MatchmakingLeaveQueue().FireAndForget(); - - closeNotifications(); - return true; - } - }); + notifications?.Post(backgroundNotification = new BackgroundQueueNotification()); } private void closeNotifications() @@ -146,13 +127,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue if (backgroundNotification != null) { backgroundNotification.State = ProgressNotificationState.Cancelled; - backgroundNotification.Close(false); + backgroundNotification.CloseAll(); + backgroundNotification = null; } - - readyNotification?.Close(false); - - backgroundNotification = null; - readyNotification = null; } protected override void Dispose(bool isDisposing) @@ -168,5 +145,78 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue client.MatchmakingRoomReady -= onMatchmakingRoomReady; } } + + private partial class BackgroundQueueNotification : ProgressNotification + { + [Resolved] + private IPerformFromScreenRunner? performer { get; set; } + + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + private Notification? foundNotification; + private Sample? matchFoundSample; + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + Text = "Searching for opponents..."; + + CompletionClickAction = () => + { + client.MatchmakingAcceptInvitation().FireAndForget(); + performer?.PerformFromScreen(s => s.Push(new IntroScreen())); + + Close(false); + return true; + }; + + CancelRequested = () => + { + client.MatchmakingLeaveQueue().FireAndForget(); + return true; + }; + + matchFoundSample = audio.Samples.Get(@"Multiplayer/Matchmaking/match-found"); + } + + protected override Notification CreateCompletionNotification() + { + // Playing here means it will play even if notification overlay is hidden. + // + // If we add support for the completion notification to be processed during gameplay, + // this can be moved inside the `MatchFoundNotification` implementation. + matchFoundSample?.Play(); + + return foundNotification = new MatchFoundNotification + { + Activated = CompletionClickAction, + Text = "Your match is ready! Click to join.", + }; + } + + public void CloseAll() + { + foundNotification?.Close(false); + Close(false); + } + + public partial class MatchFoundNotification : ProgressCompletionNotification + { + protected override IconUsage CloseButtonIcon => FontAwesome.Solid.Times; + + public MatchFoundNotification() + { + IsCritical = true; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Icon = FontAwesome.Solid.Bolt; + IconContent.Colour = ColourInfo.GradientVertical(colours.YellowDark, colours.YellowLight); + } + } + } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index 0b06a16d98..eb387b2664 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -87,7 +87,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Debug.Assert(client.LocalUser != null); if (client.LocalUser.State == MultiplayerUserState.Results) - client.ChangeState(MultiplayerUserState.Idle); + client.ChangeState(MultiplayerUserState.Idle).FireAndForget(); } protected override string ScreenTitle => "Multiplayer"; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index bbac86fd2d..16c6a46a9c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -618,7 +618,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer updateGameplayState(); if (client.LocalUser.State == MultiplayerUserState.Ready) - client.ChangeState(MultiplayerUserState.Idle); + client.ChangeState(MultiplayerUserState.Idle).FireAndForget(); break; } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index a001863780..56120120d5 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -161,7 +161,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (client.LocalUser?.State == MultiplayerUserState.Loaded) { loadingDisplay.Show(); - client.ChangeState(MultiplayerUserState.ReadyForGameplay); + client.ChangeState(MultiplayerUserState.ReadyForGameplay).FireAndForget(); } // This will pause the clock, pending the gameplay started callback from the server. diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 200e6a715d..fb9343c519 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -296,7 +296,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate // On a manual exit, set the player back to idle unless gameplay has finished. // Of note, this doesn't cover exiting using alt-f4 or menu home option. if (multiplayerClient.Room.State != MultiplayerRoomState.Open) - multiplayerClient.ChangeState(MultiplayerUserState.Idle); + multiplayerClient.ChangeState(MultiplayerUserState.Idle).FireAndForget(); return base.OnBackButton(); }